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

Turn on strict mode in tsconfig.json, some helpful lints, and required cleanup where strictness necessitates it (#5195)

* Turn on strict mode in tsconfig.json

- Add route, clusterRoute, and payloadValidatedClusterRoute helper
  functions to improve types with backend routes

- Turn on the following new lints:
  - react/jsx-first-prop-new-line
  - react/jsx-wrap-multilines
  - react/jsx-one-expression-per-line
  - react/jsx-max-props-per-line
  - react/jsx-indent
  - react/jsx-indent-props

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

* fix build

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

* Replace KubeObject scope strings with enum

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

* Revert package.json version changes

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

* revert move hostedCluster(Id)

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

* change some type param names to be not single letters

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

* remove copy-extension-themes

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

* add new make clean action

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

* fix build to not use webpack for generating types

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

* fix kube-object-menu.test.tsx

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

* fix select.test.tsx

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

* fix catalog.test.tsx

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

* revert move fileNameMigration to index

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

* fix ref logic error

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

* fix log-resource-selector.test.tsx tests

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

* fix dock-store.test.ts test by overriding createStorage to not touch file system

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

* fix cluster.test.ts tests

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

* fix kube=api.test.ts

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

* fixed hotbar-store.test.ts

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

* fix kubeconfig-manager.test.ts

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

* fix cluster-role-bindings/__tests__/dialog.test.tsx

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

* fix role-bindings/__tests__/dialog.test.tsx

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

* fix pods.test.ts

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

* fix delete-cluster-dialog.test.tsx

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

* fix daemonset.store.test.ts

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

* fix replicaset.store.test.ts

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

* fix statefulsets/dialog/dialog.test.tsx

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

* fix replicasets/scale-dialog/dialog.test.tsx

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

* fix deployments.store.test.ts

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

* fix deployments/scale/dialog.test.tsx

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

* fix cronjob.store.test.ts

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

* fix stateful-set.api.test.ts

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

* fix deployment.api.test.ts

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

* fix api-manager.test.ts

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

* fix statefulset.store.test.ts

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

* fix job.store.test.ts

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

* fix pods.store.test.ts

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

* fix scroll-spy.test.tsx

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

* fix hotbar-remove-command.test.tsx

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

* fix catalog-entity-registry.test.ts

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

* fix welcome.test.tsx

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

* fix verify-that-all-routes-have-route-component.test.ts

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

* fix pod-tolerations.test.tsx

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

* better fix for previous 3 fixes, plus also select.test.tsx

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

* fix kube-object-menu.test.tsx

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

* fix app-paths.test.ts

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

* fix dock-tabs.test.tsx

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

* fix isReactNode typing

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

* fix sub-title.tsx

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

* fix drawer.tsx

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

* fix list-layout.tsx and header.tsx

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

* fix error-boundary.tsx

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

* fix upgrade-chart/store.ts and dock-tab.store.ts

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

* fix install-chart/store.ts

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

* fix edit-resource/store.ts

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

* fix create-resource/store.ts

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

* fix namespace-select.tsx

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

* fix namespace-select-filter.tsx

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

* fix crd-list.tsx

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

* fix wrong types for extensions

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

* fix circular dependency

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

* fix circular dependency on catalogCategoryRegistry

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

* fix api-kube

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

* fix type errors, most <Select /> errors

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

* fixing more type errors

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

* some more fixing type errors

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

* convert all KubeApis to injectable with legacy global backups

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

* factor out into a common file all the exports

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

* convert all KubeObjectStores to injectable with legacy global backups

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

* fix lint

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

* remove unused legacy KubeApi globals

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

* fix bad previous commit

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

* more crash fixing

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

* try and fix behavioural tests

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

* fix sidebar-and-tab-navigation-for-core.test.tsx

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

* fix sidebar-and-tab-navigation-for-extensions.test.tsx

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

* fix navigation-using-application-menu.test.ts

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

* fix catalog.test.tsx

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

* Make ThemeStore non-singleton and fix navigation-to-terminal-preferences.test.ts

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

* extensions.test.tsx

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

* fix catalog-entity-registry.test.ts

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

* fix navigation-using-application-menu.test.ts

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

* fix log-resource-selector.test.tsx

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

* fix dock-tabs.test.tsx

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

* fix delete-cluster-dialog.test.tsx

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

* fix navigation-to-kubernetes-preferences.test.ts

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

* fix navigation-to-editor-preferences.test.ts

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

* fix navigation-to-proxy-preferences.test.ts

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

* fix navigation-using-application-menu.test.ts

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

* fix navigation-to-application-preferences.test.ts

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

* fix dock-store.test.ts

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

* fix select.test.tsx

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

* fix role-bindings/__tests__/dialog.test.tsx

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

* fix hotbar-remove-command.test.tsx

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

* fix cluster-role-bindings/__tests__/dialog.test.tsx

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

* fix navigation-to-extension-specific-preferences.test.tsx

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

* fix navigation-to-telemetry-preferences.test.tsx

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

* fix closing-preferences.test.tsx

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

* fix navigation-to-editor-preferences.test.ts

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

* fix navigation-to-proxy-preferences.test.ts

- Fix other type errors too

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

* final tweaks

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

* Add more tsconfig files, fix bug in <Catalog>

- Make all of history, navigation injectable

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

* fix type errors

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

* Convert all of kube-details-params/ and navigate/ to injectable

- This fixes a runtime error that was encountered during testing

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

* Fix runtime errors on renderer

- remove all static uses of `createPageParam` (and then removed the
  legacy global)
- Made LensRendererExtension and LensMainExtension just used
  dependencies and not the getLegacyDi
- Fixed circular dep in extension-loader

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

* Move registerStore calls to after injectMany

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

* replace all the rest of the legacy uses of apiManager

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

* fix stack overflow and cycles in DI

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

* fix NamespaceSelectFilter not opening

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

* fix WizardStep and AddNamespaceDialog

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

* fix KubeApi's not being registered

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

* cleanup WindowManager

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

* Proper fix for Wizard, fix NamespaceStore.subscribe

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

* Rewrite withTooltip to be more type correct

- Fixes mobx related "too many recursive actions" error

- Change all the uses of withTooltips to be functional components

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

* Add e2e test to cover kube api registration

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

* cleanup internal-commands

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

* remove cast in <Animate>

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

* Fix command-palette e2e test

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

* Fix type error after rebase

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

* Fix test name

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

* fix lint

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

* fix code to help CodeQL scanner

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

* update intree extension lock files

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

* Fix build-extensions picking wrong @types/react

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

* fix tests from rebase

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

* fix type error

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

* Make KubeconfigSyncManager more injectable

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

* fix crash in test mode for Dialog

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

* make Select snapshots deterministic

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

* fix new type error

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

* fix kube-object.store.test.ts typing

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

* fix merge build issues

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

* fix snapshots after merge

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

* fix lint after merge

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

* reexport BaseKubeJsonApiObjectMetadata

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

* fix typo in terminalSpawningPool

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

* remove duplicate license header

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

* fix typo to waitUntilDefined

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

* remove iter use from getLegacyGlobalDiForExtensionApi

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

* remove complex createStorage override

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

* override logger with mocks only when needed for tests

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

* remove specialized overrideStore flags for getDiForUnitTesting

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

* remove unnecessary | undefined types from the exactOptionalFieldTypes experiment

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

* use more descriptive name for local test mocks

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

* remove unnecessary addition to 'make clean' target

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

* remove oddity of KubeObjectStore.getById(undefined) being allowed

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

* rename KubeObject.getDescriptor in favour of name without fundemental JS meaning

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

* Simplify legacyRegisterApi when working in behaviour unit tests

- Don't emit within main environment as there should be no auto
  registering there

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

* change confusing variable name in ReactiveDuration

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

* make visitor pattern more explicit for Entity contextMenuOpen

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

* toggleDetails -> toggleKubeDetailsPane is more specific

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

* remove outdated comment

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

* fix bug where LensExtension dependencies are not set

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

* Fix tests from the revert of react 18

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

* fix more tests from merge

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

* fix typings with new is-compatible-extension tests

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

* more type fixing

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

* Revert in-tree extension versions

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

* Improve name of guarding injectable for stores and apis

- New name better implies that it is just a guard state and does not do
  anything

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

* Add helper for <Select>.isMulti for storing in a Set<Value>

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

* fix is-compatible-extension.test.ts types

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-05-16 07:17:57 -04:00 committed by GitHub
parent 381d77c633
commit dfcb7c3330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1062 changed files with 26516 additions and 28693 deletions

View File

@ -130,6 +130,14 @@ module.exports = {
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",
"no-restricted-imports": ["error", {
"paths": [
{
"name": ".",
"message": "No importing from local index.ts(x?) file. A common way to make circular dependencies.",
},
],
}],
"@typescript-eslint/member-delimiter-style": ["error", { "@typescript-eslint/member-delimiter-style": ["error", {
"multiline": { "multiline": {
"delimiter": "semi", "delimiter": "semi",
@ -140,6 +148,28 @@ module.exports = {
"requireLast": false, "requireLast": false,
}, },
}], }],
"react/jsx-max-props-per-line": ["error", {
"maximum": {
"single": 2,
"multi": 1,
},
}],
"react/jsx-first-prop-new-line": ["error", "multiline"],
"react/jsx-one-expression-per-line": ["error", {
"allow": "single-child",
}],
"react/jsx-indent": ["error", 2],
"react/jsx-indent-props": ["error", 2],
"react/jsx-closing-tag-location": "error",
"react/jsx-wrap-multilines": ["error", {
"declaration": "parens-new-line",
"assignment": "parens-new-line",
"return": "parens-new-line",
"arrow": "parens-new-line",
"condition": "parens-new-line",
"logical": "parens-new-line",
"prop": "parens-new-line",
}],
"react/display-name": "off", "react/display-name": "off",
"space-before-function-paren": "off", "space-before-function-paren": "off",
"@typescript-eslint/space-before-function-paren": ["error", { "@typescript-eslint/space-before-function-paren": ["error", {
@ -218,5 +248,35 @@ module.exports = {
"@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/consistent-type-imports": "error",
}, },
}, },
{
files: [
"src/{common,main,renderer}/**/*.ts",
"src/{common,main,renderer}/**/*.tsx",
],
rules: {
"no-restricted-imports": ["error", {
"paths": [
{
"name": ".",
"message": "No importing from local index.ts(x?) file. A common way to make circular dependencies.",
},
{
"name": "..",
"message": "No importing from parent index.ts(x?) file. A common way to make circular dependencies.",
},
],
"patterns": [
{
"group": [
"**/extensions/renderer-api/**/*",
"**/extensions/main-api/**/*",
"**/extensions/common-api/**/*",
],
message: "No importing from the extension api definitions in application code",
},
],
}],
},
},
], ],
}; };

View File

@ -63,6 +63,10 @@ ifeq "$(DETECTED_OS)" "Windows"
endif endif
yarn run electron-builder --publish onTag $(ELECTRON_BUILDER_EXTRA_ARGS) yarn run electron-builder --publish onTag $(ELECTRON_BUILDER_EXTRA_ARGS)
.PHONY: update-extension-locks
update-extension-locks:
$(foreach dir, $(extensions), (cd $(dir) && rm package-lock.json && ../../node_modules/.bin/npm install --package-lock-only);)
.NOTPARALLEL: $(extension_node_modules) .NOTPARALLEL: $(extension_node_modules)
$(extension_node_modules): node_modules $(extension_node_modules): node_modules
cd $(@:/node_modules=) && ../../node_modules/.bin/npm install --no-audit --no-fund --no-save cd $(@:/node_modules=) && ../../node_modules/.bin/npm install --no-audit --no-fund --no-save
@ -81,19 +85,17 @@ build-extensions: node_modules clean-old-extensions $(extension_dists)
test-extensions: $(extension_node_modules) test-extensions: $(extension_node_modules)
$(foreach dir, $(extensions), (cd $(dir) && npm run test || exit $?);) $(foreach dir, $(extensions), (cd $(dir) && npm run test || exit $?);)
.PHONY: copy-extension-themes
copy-extension-themes:
mkdir -p src/extensions/npm/extensions/dist/src/renderer/themes/
cp $(wildcard src/renderer/themes/*.json) src/extensions/npm/extensions/dist/src/renderer/themes/
src/extensions/npm/extensions/__mocks__: src/extensions/npm/extensions/__mocks__:
cp -r __mocks__ src/extensions/npm/extensions/ cp -r __mocks__ src/extensions/npm/extensions/
src/extensions/npm/extensions/dist: node_modules src/extensions/npm/extensions/dist: src/extensions/npm/extensions/node_modules
yarn compile:extension-types yarn compile:extension-types
src/extensions/npm/extensions/node_modules: src/extensions/npm/extensions/package.json
cd src/extensions/npm/extensions/ && ../../../../node_modules/.bin/npm install --no-audit --no-fund
.PHONY: build-npm .PHONY: build-npm
build-npm: build-extension-types copy-extension-themes src/extensions/npm/extensions/__mocks__ build-npm: build-extension-types src/extensions/npm/extensions/__mocks__
yarn npm:fix-package-version yarn npm:fix-package-version
.PHONY: build-extension-types .PHONY: build-extension-types

View File

@ -5,11 +5,11 @@
import fs from "fs-extra"; import fs from "fs-extra";
import path from "path"; import path from "path";
import defaultBaseLensTheme from "../src/renderer/themes/lens-dark.json"; import defaultBaseLensTheme from "../src/renderer/themes/lens-dark";
const outputCssFile = path.resolve("src/renderer/themes/theme-vars.css"); const outputCssFile = path.resolve("src/renderer/themes/theme-vars.css");
const banner = `/* const banner = `/*
Generated Lens theme CSS-variables, don't edit manually. Generated Lens theme CSS-variables, don't edit manually.
To refresh file run $: yarn run ts-node build/${path.basename(__filename)} To refresh file run $: yarn run ts-node build/${path.basename(__filename)}
*/`; */`;

View File

@ -3,9 +3,9 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import packageInfo from "../package.json"; import packageInfo from "../package.json";
import { type WriteStream } from "fs";
import type { FileHandle } from "fs/promises"; import type { FileHandle } from "fs/promises";
import { open } from "fs/promises"; import { open } from "fs/promises";
import type { WriteStream } from "fs-extra";
import { constants, ensureDir, unlink } from "fs-extra"; import { constants, ensureDir, unlink } from "fs-extra";
import path from "path"; import path from "path";
import fetch from "node-fetch"; import fetch from "node-fetch";
@ -17,6 +17,7 @@ import AbortController from "abort-controller";
import { extract } from "tar-stream"; import { extract } from "tar-stream";
import gunzip from "gunzip-maybe"; import gunzip from "gunzip-maybe";
import { getBinaryName, normalizedPlatform } from "../src/common/vars"; import { getBinaryName, normalizedPlatform } from "../src/common/vars";
import { isErrnoException } from "../src/common/utils";
const pipeline = promisify(_pipeline); const pipeline = promisify(_pipeline);
@ -44,6 +45,10 @@ abstract class BinaryDownloader {
} }
async ensureBinary(): Promise<void> { async ensureBinary(): Promise<void> {
if (process.env.LENS_SKIP_DOWNLOAD_BINARIES === "true") {
return;
}
const controller = new AbortController(); const controller = new AbortController();
const stream = await fetch(this.url, { const stream = await fetch(this.url, {
timeout: 15 * 60 * 1000, // 15min timeout: 15 * 60 * 1000, // 15min
@ -51,7 +56,7 @@ abstract class BinaryDownloader {
}); });
const total = Number(stream.headers.get("content-length")); const total = Number(stream.headers.get("content-length"));
const bar = this.bar; const bar = this.bar;
let fileHandle: FileHandle; let fileHandle: FileHandle | undefined = undefined;
if (isNaN(total)) { if (isNaN(total)) {
throw new Error("no content-length header was present"); throw new Error("no content-length header was present");
@ -66,7 +71,7 @@ abstract class BinaryDownloader {
* This is necessary because for some reason `createWriteStream({ flags: "wx" })` * This is necessary because for some reason `createWriteStream({ flags: "wx" })`
* was throwing someplace else and not here * was throwing someplace else and not here
*/ */
fileHandle = await open(this.target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL); const handle = fileHandle = await open(this.target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL);
await pipeline( await pipeline(
stream.body, stream.body,
@ -79,7 +84,7 @@ abstract class BinaryDownloader {
}), }),
...this.getTransformStreams(new Writable({ ...this.getTransformStreams(new Writable({
write(chunk, encoding, cb) { write(chunk, encoding, cb) {
fileHandle.write(chunk) handle.write(chunk)
.then(() => cb()) .then(() => cb())
.catch(cb); .catch(cb);
}, },
@ -90,7 +95,7 @@ abstract class BinaryDownloader {
} catch (error) { } catch (error) {
await fileHandle?.close(); await fileHandle?.close();
if (error.code === "EEXIST") { if (isErrnoException(error) && error.code === "EEXIST") {
bar.increment(total); // mark as finished bar.increment(total); // mark as finished
controller.abort(); // stop trying to download controller.abort(); // stop trying to download
} else { } else {

6
build/tsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"include": [
"./**/*",
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -208,14 +208,14 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
<section> <section>
<SubTitle title="Prometheus" /> <SubTitle title="Prometheus" />
<FormSwitch <FormSwitch
control={ control={(
<Switcher <Switcher
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable} disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
checked={!!this.featureStates.prometheus && this.props.cluster.status.phase == "connected"} checked={!!this.featureStates.prometheus && this.props.cluster.status.phase == "connected"}
onChange={v => this.togglePrometheus(v.target.checked)} onChange={v => this.togglePrometheus(v.target.checked)}
name="prometheus" name="prometheus"
/> />
} )}
label="Enable bundled Prometheus metrics stack" label="Enable bundled Prometheus metrics stack"
/> />
<small className="hint"> <small className="hint">
@ -226,14 +226,14 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
<section> <section>
<SubTitle title="Kube State Metrics" /> <SubTitle title="Kube State Metrics" />
<FormSwitch <FormSwitch
control={ control={(
<Switcher <Switcher
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable} disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
checked={!!this.featureStates.kubeStateMetrics && this.props.cluster.status.phase == "connected"} checked={!!this.featureStates.kubeStateMetrics && this.props.cluster.status.phase == "connected"}
onChange={v => this.toggleKubeStateMetrics(v.target.checked)} onChange={v => this.toggleKubeStateMetrics(v.target.checked)}
name="node-exporter" name="node-exporter"
/> />
} )}
label="Enable bundled kube-state-metrics stack" label="Enable bundled kube-state-metrics stack"
/> />
<small className="hint"> <small className="hint">
@ -245,14 +245,14 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
<section> <section>
<SubTitle title="Node Exporter" /> <SubTitle title="Node Exporter" />
<FormSwitch <FormSwitch
control={ control={(
<Switcher <Switcher
disabled={this.featureStates.nodeExporter === undefined || !this.isTogglable} disabled={this.featureStates.nodeExporter === undefined || !this.isTogglable}
checked={!!this.featureStates.nodeExporter && this.props.cluster.status.phase == "connected"} checked={!!this.featureStates.nodeExporter && this.props.cluster.status.phase == "connected"}
onChange={v => this.toggleNodeExporter(v.target.checked)} onChange={v => this.toggleNodeExporter(v.target.checked)}
name="node-exporter" name="node-exporter"
/> />
} )}
label="Enable bundled node-exporter stack" label="Enable bundled node-exporter stack"
/> />
<small className="hint"> <small className="hint">
@ -271,9 +271,11 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
className="w-60 h-14" className="w-60 h-14"
/> />
{this.canUpgrade && (<small className="hint"> {this.canUpgrade && (
An update is available for enabled metrics components. <small className="hint">
</small>)} An update is available for enabled metrics components.
</small>
)}
</section> </section>
</> </>
); );

File diff suppressed because it is too large Load Diff

View File

@ -68,7 +68,9 @@ export function NodeMenu(props: NodeMenuProps) {
labelOk: `Drain Node`, labelOk: `Drain Node`,
message: ( message: (
<p> <p>
Are you sure you want to drain <b>{nodeName}</b>? {"Are you sure you want to drain "}
<b>{nodeName}</b>
?
</p> </p>
), ),
}); });
@ -77,26 +79,42 @@ export function NodeMenu(props: NodeMenuProps) {
return ( return (
<> <>
<MenuItem onClick={shell}> <MenuItem onClick={shell}>
<Icon svg="ssh" interactive={toolbar} tooltip={toolbar && "Node shell"}/> <Icon
svg="ssh"
interactive={toolbar}
tooltip={toolbar && "Node shell"}
/>
<span className="title">Shell</span> <span className="title">Shell</span>
</MenuItem> </MenuItem>
{ {
node.isUnschedulable() node.isUnschedulable()
? ( ? (
<MenuItem onClick={unCordon}> <MenuItem onClick={unCordon}>
<Icon material="play_circle_filled" tooltip={toolbar && "Uncordon"} interactive={toolbar} /> <Icon
material="play_circle_filled"
tooltip={toolbar && "Uncordon"}
interactive={toolbar}
/>
<span className="title">Uncordon</span> <span className="title">Uncordon</span>
</MenuItem> </MenuItem>
) )
: ( : (
<MenuItem onClick={cordon}> <MenuItem onClick={cordon}>
<Icon material="pause_circle_filled" tooltip={toolbar && "Cordon"} interactive={toolbar} /> <Icon
material="pause_circle_filled"
tooltip={toolbar && "Cordon"}
interactive={toolbar}
/>
<span className="title">Cordon</span> <span className="title">Cordon</span>
</MenuItem> </MenuItem>
) )
} }
<MenuItem onClick={drain}> <MenuItem onClick={drain}>
<Icon material="delete_sweep" tooltip={toolbar && "Drain"} interactive={toolbar}/> <Icon
material="delete_sweep"
tooltip={toolbar && "Drain"}
interactive={toolbar}
/>
<span className="title">Drain</span> <span className="title">Drain</span>
</MenuItem> </MenuItem>
</> </>

File diff suppressed because it is too large Load Diff

View File

@ -71,7 +71,11 @@ export class PodAttachMenu extends React.Component<PodAttachMenuProps> {
return ( return (
<MenuItem onClick={Util.prevDefault(() => this.attachToPod(containers[0].name))}> <MenuItem onClick={Util.prevDefault(() => this.attachToPod(containers[0].name))}>
<Icon material="pageview" interactive={toolbar} tooltip={toolbar && "Attach to Pod"}/> <Icon
material="pageview"
interactive={toolbar}
tooltip={toolbar && "Attach to Pod"}
/>
<span className="title">Attach Pod</span> <span className="title">Attach Pod</span>
{containers.length > 1 && ( {containers.length > 1 && (
<> <>
@ -82,7 +86,11 @@ export class PodAttachMenu extends React.Component<PodAttachMenuProps> {
const { name } = container; const { name } = container;
return ( return (
<MenuItem key={name} onClick={Util.prevDefault(() => this.attachToPod(name))} className="flex align-center"> <MenuItem
key={name}
onClick={Util.prevDefault(() => this.attachToPod(name))}
className="flex align-center"
>
<StatusBrick/> <StatusBrick/>
<span>{name}</span> <span>{name}</span>
</MenuItem> </MenuItem>

View File

@ -46,7 +46,11 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
return ( return (
<MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}> <MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
<Icon material="subject" interactive={toolbar} tooltip={toolbar && "Pod Logs"}/> <Icon
material="subject"
interactive={toolbar}
tooltip={toolbar && "Pod Logs"}
/>
<span className="title">Logs</span> <span className="title">Logs</span>
{containers.length > 1 && ( {containers.length > 1 && (
<> <>
@ -63,7 +67,11 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
) : null; ) : null;
return ( return (
<MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center"> <MenuItem
key={name}
onClick={Util.prevDefault(() => this.showLogs(container))}
className="flex align-center"
>
{brick} {brick}
<span>{name}</span> <span>{name}</span>
</MenuItem> </MenuItem>

View File

@ -79,7 +79,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
return ( return (
<MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}> <MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
<Icon svg="ssh" interactive={toolbar} tooltip={toolbar && "Pod Shell"} /> <Icon
svg="ssh"
interactive={toolbar}
tooltip={toolbar && "Pod Shell"}
/>
<span className="title">Shell</span> <span className="title">Shell</span>
{containers.length > 1 && ( {containers.length > 1 && (
<> <>
@ -90,7 +94,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
const { name } = container; const { name } = container;
return ( return (
<MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center"> <MenuItem
key={name}
onClick={Util.prevDefault(() => this.execShell(name))}
className="flex align-center"
>
<StatusBrick/> <StatusBrick/>
<span>{name}</span> <span>{name}</span>
</MenuItem> </MenuItem>

View File

@ -47,7 +47,6 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
it( it(
"should navigate around common cluster pages", "should navigate around common cluster pages",
async () => { async () => {
const scenariosByParent = pipeline( const scenariosByParent = pipeline(
scenarios, scenarios,
@ -139,7 +138,7 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
); );
it( it(
`should create the ${TEST_NAMESPACE} and a pod in the namespace`, `should create the ${TEST_NAMESPACE} and a pod in the namespace and then remove that pod via the context menu`,
async () => { async () => {
await navigateToNamespaces(frame); await navigateToNamespaces(frame);
await frame.click("button.add-button"); await frame.click("button.add-button");
@ -209,6 +208,10 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
await frame.click(".Dock .Button >> text='Create'"); await frame.click(".Dock .Button >> text='Create'");
await frame.waitForSelector(`.TableCell >> text=${testPodName}`); await frame.waitForSelector(`.TableCell >> text=${testPodName}`);
await frame.click(".TableRow .TableCell.menu");
await frame.click(".MenuItem >> text=Delete");
await frame.click("button >> text=Remove");
await frame.waitForSelector(`.TableCell >> text=${testPodName}`, { state: "detached" });
}, },
10 * 60 * 1000, 10 * 60 * 1000,
); );

View File

@ -24,9 +24,9 @@ describe("Lens command palette", () => {
utils.itIf(!isWindows)("opens command dialog from menu", async () => { utils.itIf(!isWindows)("opens command dialog from menu", async () => {
await app.evaluate(async ({ app }) => { await app.evaluate(async ({ app }) => {
await app.applicationMenu await app.applicationMenu
.getMenuItemById("view") ?.getMenuItemById("view")
.submenu.getMenuItemById("command-palette") ?.submenu?.getMenuItemById("command-palette")
.click(); ?.click();
}); });
await window.waitForSelector(".Select__option >> text=Hotbar: Switch"); await window.waitForSelector(".Select__option >> text=Hotbar: Switch");
}, 10*60*1000); }, 10*60*1000);

View File

@ -108,6 +108,10 @@ export async function lauchMinikubeClusterFromCatalog(window: Page): Promise<Fra
const frame = await minikubeFrame.contentFrame(); const frame = await minikubeFrame.contentFrame();
if (!frame) {
throw new Error("No iframe for minikube found");
}
await frame.waitForSelector("[data-testid=cluster-sidebar]"); await frame.waitForSelector("[data-testid=cluster-sidebar]");
return frame; return frame;

View File

@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"include": [
"./**/*",
]
}

View File

@ -21,7 +21,7 @@
"compile": "env NODE_ENV=production concurrently yarn:compile:*", "compile": "env NODE_ENV=production concurrently yarn:compile:*",
"compile:main": "yarn run webpack --config webpack/main.ts", "compile:main": "yarn run webpack --config webpack/main.ts",
"compile:renderer": "yarn run webpack --config webpack/renderer.ts", "compile:renderer": "yarn run webpack --config webpack/renderer.ts",
"compile:extension-types": "yarn run webpack --config webpack/extensions.ts", "compile:extension-types": "yarn run tsc --project tsconfig.extension-api.json",
"npm:fix-build-version": "yarn run ts-node build/set_build_version.ts", "npm:fix-build-version": "yarn run ts-node build/set_build_version.ts",
"npm:fix-package-version": "yarn run ts-node build/set_npm_version.ts", "npm:fix-package-version": "yarn run ts-node build/set_npm_version.ts",
"build:linux": "yarn run compile && electron-builder --linux --dir", "build:linux": "yarn run compile && electron-builder --linux --dir",
@ -203,6 +203,7 @@
"@hapi/call": "^8.0.1", "@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3", "@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.16.3", "@kubernetes/client-node": "^0.16.3",
"@material-ui/styles": "^4.11.5",
"@ogre-tools/fp": "5.2.0", "@ogre-tools/fp": "5.2.0",
"@ogre-tools/injectable": "5.2.0", "@ogre-tools/injectable": "5.2.0",
"@ogre-tools/injectable-react": "5.2.0", "@ogre-tools/injectable-react": "5.2.0",
@ -265,13 +266,14 @@
"tar": "^6.1.11", "tar": "^6.1.11",
"tcp-port-used": "^1.0.2", "tcp-port-used": "^1.0.2",
"tempy": "1.0.1", "tempy": "1.0.1",
"typed-regex": "^0.0.8",
"url-parse": "^1.5.10", "url-parse": "^1.5.10",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"win-ca": "^3.5.0", "win-ca": "^3.5.0",
"winston": "^3.7.2", "winston": "^3.7.2",
"winston-console-format": "^1.0.8", "winston-console-format": "^1.0.8",
"winston-transport-browserconsole": "^1.0.5", "winston-transport-browserconsole": "^1.0.5",
"ws": "^7.5.7" "ws": "^8.5.0"
}, },
"devDependencies": { "devDependencies": {
"@async-fn/jest": "1.5.3", "@async-fn/jest": "1.5.3",
@ -280,11 +282,13 @@
"@material-ui/lab": "^4.0.0-alpha.60", "@material-ui/lab": "^4.0.0-alpha.60",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
"@sentry/types": "^6.19.7", "@sentry/types": "^6.19.7",
"@testing-library/dom": "^7.31.2",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.5", "@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/byline": "^4.2.33", "@types/byline": "^4.2.33",
"@types/chart.js": "^2.9.36", "@types/chart.js": "^2.9.36",
"@types/circular-dependency-plugin": "5.0.5",
"@types/cli-progress": "^3.9.2", "@types/cli-progress": "^3.9.2",
"@types/color": "^3.0.3", "@types/color": "^3.0.3",
"@types/command-line-args": "^5.2.0", "@types/command-line-args": "^5.2.0",
@ -294,7 +298,6 @@
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/glob-to-regexp": "^0.4.1", "@types/glob-to-regexp": "^0.4.1",
"@types/gunzip-maybe": "^1.4.0", "@types/gunzip-maybe": "^1.4.0",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/html-webpack-plugin": "^3.2.6", "@types/html-webpack-plugin": "^3.2.6",
"@types/http-proxy": "^1.17.9", "@types/http-proxy": "^1.17.9",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
@ -310,9 +313,10 @@
"@types/npm": "^2.0.32", "@types/npm": "^2.0.32",
"@types/proper-lockfile": "^4.1.2", "@types/proper-lockfile": "^4.1.2",
"@types/randomcolor": "^0.5.6", "@types/randomcolor": "^0.5.6",
"@types/react": "^17.0.44", "@types/react": "^17.0.45",
"@types/react-beautiful-dnd": "^13.1.2", "@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.14", "@types/react-dom": "^17.0.16",
"@types/react-router": "^5.1.18",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/react-table": "^7.7.11", "@types/react-table": "^7.7.11",
"@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-virtualized-auto-sizer": "^1.0.1",
@ -360,7 +364,6 @@
"flex.box": "^3.4.4", "flex.box": "^3.4.4",
"fork-ts-checker-webpack-plugin": "^6.5.0", "fork-ts-checker-webpack-plugin": "^6.5.0",
"gunzip-maybe": "^1.4.2", "gunzip-maybe": "^1.4.2",
"hoist-non-react-statics": "^3.3.2",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"ignore-loader": "^0.1.2", "ignore-loader": "^0.1.2",

View File

@ -19,7 +19,7 @@ exports[`add-cluster - navigation using application menu when navigating to add
Add Clusters from Kubeconfig Add Clusters from Kubeconfig
</h2> </h2>
<p> <p>
Clusters added here are Clusters added here are
<b> <b>
not not
</b> </b>
@ -27,16 +27,14 @@ exports[`add-cluster - navigation using application menu when navigating to add
<code> <code>
~/.kube/config ~/.kube/config
</code> </code>
file. file.
<a <a
href="https://docs.k8slens.dev/main//catalog/add-clusters/" href="https://docs.k8slens.dev/main//catalog/add-clusters/"
rel="noreferrer" rel="noreferrer"
target="_blank" target="_blank"
> >
Read more about adding clusters Read more about adding clusters.
</a> </a>
.
</p> </p>
<div <div
class="flex column" class="flex column"

View File

@ -7,10 +7,18 @@ import type { RenderResult } from "@testing-library/react";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable"; import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable";
import React from "react";
// TODO: Make components free of side effects by making them deterministic // TODO: Make components free of side effects by making them deterministic
jest.mock("../../renderer/components/tooltip"); jest.mock("../../renderer/components/tooltip/tooltip", () => ({
jest.mock("../../renderer/components/monaco-editor/monaco-editor"); Tooltip: () => null,
}));
jest.mock("../../renderer/components/tooltip/withTooltip", () => ({
withTooltip: (Target: any) => ({ tooltip, tooltipOverrideDisabled, ...props }: any) => <Target {...props} />,
}));
jest.mock("../../renderer/components/monaco-editor/monaco-editor", () => ({
MonacoEditor: () => null,
}));
describe("add-cluster - navigation using application menu", () => { describe("add-cluster - navigation using application menu", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;

View File

@ -261,14 +261,14 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-parent-id" data-id-test="some-extension-name-some-parent-id"
data-is-active-test="false" data-is-active-test="false"
data-test-id="some-extension-id-some-parent-id" data-test-id="some-extension-name-some-parent-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
class="nav-item flex gaps align-center expandable" class="nav-item flex gaps align-center expandable"
data-testid="sidebar-item-link-for-some-extension-id-some-parent-id" data-testid="sidebar-item-link-for-some-extension-name-some-parent-id"
href="/" href="/"
> >
<div> <div>
@ -557,14 +557,14 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-parent-id" data-id-test="some-extension-name-some-parent-id"
data-is-active-test="false" data-is-active-test="false"
data-test-id="some-extension-id-some-parent-id" data-test-id="some-extension-name-some-parent-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
class="nav-item flex gaps align-center expandable" class="nav-item flex gaps align-center expandable"
data-testid="sidebar-item-link-for-some-extension-id-some-parent-id" data-testid="sidebar-item-link-for-some-extension-name-some-parent-id"
href="/" href="/"
> >
<div> <div>
@ -853,14 +853,14 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-parent-id" data-id-test="some-extension-name-some-parent-id"
data-is-active-test="false" data-is-active-test="false"
data-test-id="some-extension-id-some-parent-id" data-test-id="some-extension-name-some-parent-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
class="nav-item flex gaps align-center expandable" class="nav-item flex gaps align-center expandable"
data-testid="sidebar-item-link-for-some-extension-id-some-parent-id" data-testid="sidebar-item-link-for-some-extension-name-some-parent-id"
href="/" href="/"
> >
<div> <div>
@ -887,15 +887,15 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
> >
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-child-id" data-id-test="some-extension-name-some-child-id"
data-is-active-test="false" data-is-active-test="false"
data-parent-id-test="some-extension-id-some-parent-id" data-parent-id-test="some-extension-name-some-parent-id"
data-test-id="some-extension-id-some-child-id" data-test-id="some-extension-name-some-child-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
class="nav-item flex gaps align-center" class="nav-item flex gaps align-center"
data-testid="sidebar-item-link-for-some-extension-id-some-child-id" data-testid="sidebar-item-link-for-some-extension-name-some-child-id"
href="/" href="/"
> >
<span <span
@ -907,15 +907,15 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-other-child-id" data-id-test="some-extension-name-some-other-child-id"
data-is-active-test="false" data-is-active-test="false"
data-parent-id-test="some-extension-id-some-parent-id" data-parent-id-test="some-extension-name-some-parent-id"
data-test-id="some-extension-id-some-other-child-id" data-test-id="some-extension-name-some-other-child-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
class="nav-item flex gaps align-center" class="nav-item flex gaps align-center"
data-testid="sidebar-item-link-for-some-extension-id-some-other-child-id" data-testid="sidebar-item-link-for-some-extension-name-some-other-child-id"
href="/" href="/"
> >
<span <span
@ -1193,15 +1193,15 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-parent-id" data-id-test="some-extension-name-some-parent-id"
data-is-active-test="true" data-is-active-test="true"
data-test-id="some-extension-id-some-parent-id" data-test-id="some-extension-name-some-parent-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
aria-current="page" aria-current="page"
class="nav-item flex gaps align-center expandable active" class="nav-item flex gaps align-center expandable active"
data-testid="sidebar-item-link-for-some-extension-id-some-parent-id" data-testid="sidebar-item-link-for-some-extension-name-some-parent-id"
href="/" href="/"
> >
<div> <div>
@ -1228,16 +1228,16 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
> >
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-child-id" data-id-test="some-extension-name-some-child-id"
data-is-active-test="true" data-is-active-test="true"
data-parent-id-test="some-extension-id-some-parent-id" data-parent-id-test="some-extension-name-some-parent-id"
data-test-id="some-extension-id-some-child-id" data-test-id="some-extension-name-some-child-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
aria-current="page" aria-current="page"
class="nav-item flex gaps align-center active" class="nav-item flex gaps align-center active"
data-testid="sidebar-item-link-for-some-extension-id-some-child-id" data-testid="sidebar-item-link-for-some-extension-name-some-child-id"
href="/" href="/"
> >
<span <span
@ -1249,15 +1249,15 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-other-child-id" data-id-test="some-extension-name-some-other-child-id"
data-is-active-test="false" data-is-active-test="false"
data-parent-id-test="some-extension-id-some-parent-id" data-parent-id-test="some-extension-name-some-parent-id"
data-test-id="some-extension-id-some-other-child-id" data-test-id="some-extension-name-some-other-child-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
class="nav-item flex gaps align-center" class="nav-item flex gaps align-center"
data-testid="sidebar-item-link-for-some-extension-id-some-other-child-id" data-testid="sidebar-item-link-for-some-extension-name-some-other-child-id"
href="/" href="/"
> >
<span <span
@ -1281,7 +1281,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
<div <div
class="Tab flex gaps align-center active" class="Tab flex gaps align-center active"
data-is-active-test="true" data-is-active-test="true"
data-testid="tab-link-for-some-extension-id-some-child-id" data-testid="tab-link-for-some-extension-name-some-child-id"
role="tab" role="tab"
tabindex="0" tabindex="0"
> >
@ -1294,7 +1294,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
<div <div
class="Tab flex gaps align-center" class="Tab flex gaps align-center"
data-is-active-test="false" data-is-active-test="false"
data-testid="tab-link-for-some-extension-id-some-other-child-id" data-testid="tab-link-for-some-extension-name-some-other-child-id"
role="tab" role="tab"
tabindex="0" tabindex="0"
> >
@ -1577,15 +1577,15 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-parent-id" data-id-test="some-extension-name-some-parent-id"
data-is-active-test="true" data-is-active-test="true"
data-test-id="some-extension-id-some-parent-id" data-test-id="some-extension-name-some-parent-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
aria-current="page" aria-current="page"
class="nav-item flex gaps align-center expandable active" class="nav-item flex gaps align-center expandable active"
data-testid="sidebar-item-link-for-some-extension-id-some-parent-id" data-testid="sidebar-item-link-for-some-extension-name-some-parent-id"
href="/" href="/"
> >
<div> <div>
@ -1612,15 +1612,15 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
> >
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-child-id" data-id-test="some-extension-name-some-child-id"
data-is-active-test="false" data-is-active-test="false"
data-parent-id-test="some-extension-id-some-parent-id" data-parent-id-test="some-extension-name-some-parent-id"
data-test-id="some-extension-id-some-child-id" data-test-id="some-extension-name-some-child-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
class="nav-item flex gaps align-center" class="nav-item flex gaps align-center"
data-testid="sidebar-item-link-for-some-extension-id-some-child-id" data-testid="sidebar-item-link-for-some-extension-name-some-child-id"
href="/" href="/"
> >
<span <span
@ -1632,16 +1632,16 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-other-child-id" data-id-test="some-extension-name-some-other-child-id"
data-is-active-test="true" data-is-active-test="true"
data-parent-id-test="some-extension-id-some-parent-id" data-parent-id-test="some-extension-name-some-parent-id"
data-test-id="some-extension-id-some-other-child-id" data-test-id="some-extension-name-some-other-child-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
aria-current="page" aria-current="page"
class="nav-item flex gaps align-center active" class="nav-item flex gaps align-center active"
data-testid="sidebar-item-link-for-some-extension-id-some-other-child-id" data-testid="sidebar-item-link-for-some-extension-name-some-other-child-id"
href="/" href="/"
> >
<span <span
@ -1665,7 +1665,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
<div <div
class="Tab flex gaps align-center" class="Tab flex gaps align-center"
data-is-active-test="false" data-is-active-test="false"
data-testid="tab-link-for-some-extension-id-some-child-id" data-testid="tab-link-for-some-extension-name-some-child-id"
role="tab" role="tab"
tabindex="0" tabindex="0"
> >
@ -1678,7 +1678,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
<div <div
class="Tab flex gaps align-center active" class="Tab flex gaps align-center active"
data-is-active-test="true" data-is-active-test="true"
data-testid="tab-link-for-some-extension-id-some-other-child-id" data-testid="tab-link-for-some-extension-name-some-other-child-id"
role="tab" role="tab"
tabindex="0" tabindex="0"
> >
@ -1961,15 +1961,15 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-parent-id" data-id-test="some-extension-name-some-parent-id"
data-is-active-test="true" data-is-active-test="true"
data-test-id="some-extension-id-some-parent-id" data-test-id="some-extension-name-some-parent-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
aria-current="page" aria-current="page"
class="nav-item flex gaps align-center expandable active" class="nav-item flex gaps align-center expandable active"
data-testid="sidebar-item-link-for-some-extension-id-some-parent-id" data-testid="sidebar-item-link-for-some-extension-name-some-parent-id"
href="/" href="/"
> >
<div> <div>
@ -2004,7 +2004,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
<div <div
class="Tab flex gaps align-center active" class="Tab flex gaps align-center active"
data-is-active-test="true" data-is-active-test="true"
data-testid="tab-link-for-some-extension-id-some-child-id" data-testid="tab-link-for-some-extension-name-some-child-id"
role="tab" role="tab"
tabindex="0" tabindex="0"
> >
@ -2017,7 +2017,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
<div <div
class="Tab flex gaps align-center" class="Tab flex gaps align-center"
data-is-active-test="false" data-is-active-test="false"
data-testid="tab-link-for-some-extension-id-some-other-child-id" data-testid="tab-link-for-some-extension-name-some-other-child-id"
role="tab" role="tab"
tabindex="0" tabindex="0"
> >
@ -2300,14 +2300,14 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-parent-id" data-id-test="some-extension-name-some-parent-id"
data-is-active-test="false" data-is-active-test="false"
data-test-id="some-extension-id-some-parent-id" data-test-id="some-extension-name-some-parent-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
class="nav-item flex gaps align-center expandable" class="nav-item flex gaps align-center expandable"
data-testid="sidebar-item-link-for-some-extension-id-some-parent-id" data-testid="sidebar-item-link-for-some-extension-name-some-parent-id"
href="/" href="/"
> >
<div> <div>
@ -2334,15 +2334,15 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
> >
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-child-id" data-id-test="some-extension-name-some-child-id"
data-is-active-test="false" data-is-active-test="false"
data-parent-id-test="some-extension-id-some-parent-id" data-parent-id-test="some-extension-name-some-parent-id"
data-test-id="some-extension-id-some-child-id" data-test-id="some-extension-name-some-child-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
class="nav-item flex gaps align-center" class="nav-item flex gaps align-center"
data-testid="sidebar-item-link-for-some-extension-id-some-child-id" data-testid="sidebar-item-link-for-some-extension-name-some-child-id"
href="/" href="/"
> >
<span <span
@ -2354,15 +2354,15 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-other-child-id" data-id-test="some-extension-name-some-other-child-id"
data-is-active-test="false" data-is-active-test="false"
data-parent-id-test="some-extension-id-some-parent-id" data-parent-id-test="some-extension-name-some-parent-id"
data-test-id="some-extension-id-some-other-child-id" data-test-id="some-extension-name-some-other-child-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
class="nav-item flex gaps align-center" class="nav-item flex gaps align-center"
data-testid="sidebar-item-link-for-some-extension-id-some-other-child-id" data-testid="sidebar-item-link-for-some-extension-name-some-other-child-id"
href="/" href="/"
> >
<span <span
@ -2640,14 +2640,14 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
<div <div
class="SidebarItem" class="SidebarItem"
data-id-test="some-extension-id-some-parent-id" data-id-test="some-extension-name-some-parent-id"
data-is-active-test="false" data-is-active-test="false"
data-test-id="some-extension-id-some-parent-id" data-test-id="some-extension-name-some-parent-id"
data-testid="sidebar-item" data-testid="sidebar-item"
> >
<a <a
class="nav-item flex gaps align-center expandable" class="nav-item flex gaps align-center expandable"
data-testid="sidebar-item-link-for-some-extension-id-some-parent-id" data-testid="sidebar-item-link-for-some-extension-name-some-parent-id"
href="/" href="/"
> >
<div> <div>

View File

@ -21,6 +21,8 @@ import writeJsonFileInjectable from "../../common/fs/write-json-file.injectable"
import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import pathExistsInjectable from "../../common/fs/path-exists.injectable";
import readJsonFileInjectable from "../../common/fs/read-json-file.injectable"; import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token"; import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token";
import { getSidebarItem } from "../utils";
import sidebarStorageInjectable from "../../renderer/components/layout/sidebar-storage/sidebar-storage.injectable";
describe("cluster - sidebar and tab navigation for core", () => { describe("cluster - sidebar and tab navigation for core", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -34,7 +36,6 @@ describe("cluster - sidebar and tab navigation for core", () => {
rendererDi = applicationBuilder.dis.rendererDi; rendererDi = applicationBuilder.dis.rendererDi;
applicationBuilder.setEnvironmentToClusterFrame(); applicationBuilder.setEnvironmentToClusterFrame();
applicationBuilder.beforeSetups(({ rendererDi }) => { applicationBuilder.beforeSetups(({ rendererDi }) => {
rendererDi.override( rendererDi.override(
directoryForLensLocalStorageInjectable, directoryForLensLocalStorageInjectable,
@ -72,13 +73,13 @@ describe("cluster - sidebar and tab navigation for core", () => {
it("parent is highlighted", () => { it("parent is highlighted", () => {
const parent = getSidebarItem(rendered, "some-parent-id"); const parent = getSidebarItem(rendered, "some-parent-id");
expect(parent.dataset.isActiveTest).toBe("true"); expect(parent?.dataset.isActiveTest).toBe("true");
}); });
it("parent sidebar item is not expanded", () => { it("parent sidebar item is not expanded", () => {
const child = getSidebarItem(rendered, "some-child-id"); const child = getSidebarItem(rendered, "some-child-id");
expect(child).toBe(null); expect(child).toBeUndefined();
}); });
it("child page is shown", () => { it("child page is shown", () => {
@ -101,6 +102,11 @@ describe("cluster - sidebar and tab navigation for core", () => {
}, },
); );
}); });
applicationBuilder.beforeRender(async ({ rendererDi }) => {
const sidebarStorage = rendererDi.inject(sidebarStorageInjectable);
await sidebarStorage.whenReady;
});
rendered = await applicationBuilder.render(); rendered = await applicationBuilder.render();
}); });
@ -112,13 +118,13 @@ describe("cluster - sidebar and tab navigation for core", () => {
it("parent sidebar item is not highlighted", () => { it("parent sidebar item is not highlighted", () => {
const parent = getSidebarItem(rendered, "some-parent-id"); const parent = getSidebarItem(rendered, "some-parent-id");
expect(parent.dataset.isActiveTest).toBe("false"); expect(parent?.dataset.isActiveTest).toBe("false");
}); });
it("parent sidebar item is expanded", () => { it("parent sidebar item is expanded", () => {
const child = getSidebarItem(rendered, "some-child-id"); const child = getSidebarItem(rendered, "some-child-id");
expect(child).not.toBe(null); expect(child).not.toBeUndefined();
}); });
}); });
@ -148,7 +154,7 @@ describe("cluster - sidebar and tab navigation for core", () => {
it("parent sidebar item is not expanded", () => { it("parent sidebar item is not expanded", () => {
const child = getSidebarItem(rendered, "some-child-id"); const child = getSidebarItem(rendered, "some-child-id");
expect(child).toBe(null); expect(child).toBeUndefined();
}); });
}); });
@ -175,7 +181,7 @@ describe("cluster - sidebar and tab navigation for core", () => {
it("parent sidebar item is not expanded", () => { it("parent sidebar item is not expanded", () => {
const child = getSidebarItem(rendered, "some-child-id"); const child = getSidebarItem(rendered, "some-child-id");
expect(child).toBe(null); expect(child).toBeUndefined();
}); });
}); });
@ -191,13 +197,13 @@ describe("cluster - sidebar and tab navigation for core", () => {
it("parent sidebar item is not highlighted", () => { it("parent sidebar item is not highlighted", () => {
const parent = getSidebarItem(rendered, "some-parent-id"); const parent = getSidebarItem(rendered, "some-parent-id");
expect(parent.dataset.isActiveTest).toBe("false"); expect(parent?.dataset.isActiveTest).toBe("false");
}); });
it("parent sidebar item is not expanded", () => { it("parent sidebar item is not expanded", () => {
const child = getSidebarItem(rendered, "some-child-id"); const child = getSidebarItem(rendered, "some-child-id");
expect(child).toBe(null); expect(child).toBeUndefined();
}); });
describe("when a parent sidebar item is expanded", () => { describe("when a parent sidebar item is expanded", () => {
@ -216,13 +222,13 @@ describe("cluster - sidebar and tab navigation for core", () => {
it("parent sidebar item is not highlighted", () => { it("parent sidebar item is not highlighted", () => {
const parent = getSidebarItem(rendered, "some-parent-id"); const parent = getSidebarItem(rendered, "some-parent-id");
expect(parent.dataset.isActiveTest).toBe("false"); expect(parent?.dataset.isActiveTest).toBe("false");
}); });
it("parent sidebar item is expanded", () => { it("parent sidebar item is expanded", () => {
const child = getSidebarItem(rendered, "some-child-id"); const child = getSidebarItem(rendered, "some-child-id");
expect(child).not.toBe(null); expect(child).not.toBeUndefined();
}); });
describe("when a child of the parent is selected", () => { describe("when a child of the parent is selected", () => {
@ -241,13 +247,13 @@ describe("cluster - sidebar and tab navigation for core", () => {
it("parent is highlighted", () => { it("parent is highlighted", () => {
const parent = getSidebarItem(rendered, "some-parent-id"); const parent = getSidebarItem(rendered, "some-parent-id");
expect(parent.dataset.isActiveTest).toBe("true"); expect(parent?.dataset.isActiveTest).toBe("true");
}); });
it("child is highlighted", () => { it("child is highlighted", () => {
const child = getSidebarItem(rendered, "some-child-id"); const child = getSidebarItem(rendered, "some-child-id");
expect(child.dataset.isActiveTest).toBe("true"); expect(child?.dataset.isActiveTest).toBe("true");
}); });
it("child page is shown", () => { it("child page is shown", () => {
@ -288,11 +294,6 @@ describe("cluster - sidebar and tab navigation for core", () => {
}); });
}); });
const getSidebarItem = (rendered: RenderResult, itemId: string) =>
rendered
.queryAllByTestId("sidebar-item")
.find((x) => x.dataset.idTest === itemId) || null;
const testSidebarItemsInjectable = getInjectable({ const testSidebarItemsInjectable = getInjectable({
id: "some-sidebar-items-injectable", id: "some-sidebar-items-injectable",
@ -325,7 +326,7 @@ const testSidebarItemsInjectable = getInjectable({
injectionToken: sidebarItemsInjectionToken, injectionToken: sidebarItemsInjectionToken,
}); });
const testRouteInjectable = getInjectable({ const testRouteInjectable = getInjectable({
id: "some-route-injectable-id", id: "some-route-injectable-id",
instantiate: () => ({ instantiate: () => ({

View File

@ -5,8 +5,6 @@
import React from "react"; import React from "react";
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react"; import { fireEvent } from "@testing-library/react";
import { getRendererExtensionFake } from "../../renderer/components/test-utils/get-renderer-extension-fake";
import type { LensRendererExtension } from "../../extensions/lens-renderer-extension";
import directoryForLensLocalStorageInjectable from "../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; import directoryForLensLocalStorageInjectable from "../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import routesInjectable from "../../renderer/routes/routes.injectable"; import routesInjectable from "../../renderer/routes/routes.injectable";
import { matches } from "lodash/fp"; import { matches } from "lodash/fp";
@ -17,6 +15,10 @@ import pathExistsInjectable from "../../common/fs/path-exists.injectable";
import readJsonFileInjectable from "../../common/fs/read-json-file.injectable"; import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token"; import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token";
import assert from "assert";
import { getSidebarItem } from "../utils";
import type { FakeExtensionData } from "../../renderer/components/test-utils/get-renderer-extension-fake";
import { getRendererExtensionFakeFor } from "../../renderer/components/test-utils/get-renderer-extension-fake";
describe("cluster - sidebar and tab navigation for extensions", () => { describe("cluster - sidebar and tab navigation for extensions", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -41,9 +43,8 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
describe("given extension with cluster pages and cluster page menus", () => { describe("given extension with cluster pages and cluster page menus", () => {
beforeEach(async () => { beforeEach(async () => {
const testExtension = getRendererExtensionFake( const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder);
extensionStubWithSidebarItems, const testExtension = getRendererExtensionFake(extensionStubWithSidebarItems);
);
await applicationBuilder.addExtensions(testExtension); await applicationBuilder.addExtensions(testExtension);
}); });
@ -51,19 +52,17 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
describe("given no state for expanded sidebar items exists, and navigated to child sidebar item, when rendered", () => { describe("given no state for expanded sidebar items exists, and navigated to child sidebar item, when rendered", () => {
beforeEach(async () => { beforeEach(async () => {
applicationBuilder.beforeRender(({ rendererDi }) => { applicationBuilder.beforeRender(({ rendererDi }) => {
const navigateToRoute = rendererDi.inject( const navigateToRoute = rendererDi.inject(navigateToRouteInjectionToken);
navigateToRouteInjectionToken,
);
const route = rendererDi const route = rendererDi
.inject(routesInjectable) .inject(routesInjectable)
.get() .get()
.find( .find(
matches({ matches({
path: "/extension/some-extension-id/some-child-page-id", path: "/extension/some-extension-name/some-child-page-id",
}), }),
); );
assert(route);
navigateToRoute(route); navigateToRoute(route);
}); });
@ -77,19 +76,19 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
it("parent is highlighted", () => { it("parent is highlighted", () => {
const parent = getSidebarItem( const parent = getSidebarItem(
rendered, rendered,
"some-extension-id-some-parent-id", "some-extension-name-some-parent-id",
); );
expect(parent.dataset.isActiveTest).toBe("true"); expect(parent?.dataset.isActiveTest).toBe("true");
}); });
it("parent sidebar item is not expanded", () => { it("parent sidebar item is not expanded", () => {
const child = getSidebarItem( const child = getSidebarItem(
rendered, rendered,
"some-extension-id-some-child-id", "some-extension-name-some-child-id",
); );
expect(child).toBe(null); expect(child).toBeUndefined();
}); });
it("child page is shown", () => { it("child page is shown", () => {
@ -106,7 +105,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
"/some-directory-for-lens-local-storage/app.json", "/some-directory-for-lens-local-storage/app.json",
{ {
sidebar: { sidebar: {
expanded: { "some-extension-id-some-parent-id": true }, expanded: { "some-extension-name-some-parent-id": true },
width: 200, width: 200,
}, },
}, },
@ -123,19 +122,19 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
it("parent sidebar item is not highlighted", () => { it("parent sidebar item is not highlighted", () => {
const parent = getSidebarItem( const parent = getSidebarItem(
rendered, rendered,
"some-extension-id-some-parent-id", "some-extension-name-some-parent-id",
); );
expect(parent.dataset.isActiveTest).toBe("false"); expect(parent?.dataset.isActiveTest).toBe("false");
}); });
it("parent sidebar item is expanded", () => { it("parent sidebar item is expanded", () => {
const child = getSidebarItem( const child = getSidebarItem(
rendered, rendered,
"some-extension-id-some-child-id", "some-extension-name-some-child-id",
); );
expect(child).not.toBe(null); expect(child).not.toBeUndefined();
}); });
}); });
@ -148,7 +147,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
"/some-directory-for-lens-local-storage/app.json", "/some-directory-for-lens-local-storage/app.json",
{ {
sidebar: { sidebar: {
expanded: { "some-extension-id-some-unknown-parent-id": true }, expanded: { "some-extension-name-some-unknown-parent-id": true },
width: 200, width: 200,
}, },
}, },
@ -165,10 +164,10 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
it("parent sidebar item is not expanded", () => { it("parent sidebar item is not expanded", () => {
const child = getSidebarItem( const child = getSidebarItem(
rendered, rendered,
"some-extension-id-some-child-id", "some-extension-name-some-child-id",
); );
expect(child).toBe(null); expect(child).toBeUndefined();
}); });
}); });
@ -195,10 +194,10 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
it("parent sidebar item is not expanded", () => { it("parent sidebar item is not expanded", () => {
const child = getSidebarItem( const child = getSidebarItem(
rendered, rendered,
"some-extension-id-some-child-id", "some-extension-name-some-child-id",
); );
expect(child).toBe(null); expect(child).toBeUndefined();
}); });
}); });
@ -214,25 +213,25 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
it("parent sidebar item is not highlighted", () => { it("parent sidebar item is not highlighted", () => {
const parent = getSidebarItem( const parent = getSidebarItem(
rendered, rendered,
"some-extension-id-some-parent-id", "some-extension-name-some-parent-id",
); );
expect(parent.dataset.isActiveTest).toBe("false"); expect(parent?.dataset.isActiveTest).toBe("false");
}); });
it("parent sidebar item is not expanded", () => { it("parent sidebar item is not expanded", () => {
const child = getSidebarItem( const child = getSidebarItem(
rendered, rendered,
"some-extension-id-some-child-id", "some-extension-name-some-child-id",
); );
expect(child).toBe(null); expect(child).toBeUndefined();
}); });
describe("when a parent sidebar item is expanded", () => { describe("when a parent sidebar item is expanded", () => {
beforeEach(() => { beforeEach(() => {
const parentLink = rendered.getByTestId( const parentLink = rendered.getByTestId(
"sidebar-item-link-for-some-extension-id-some-parent-id", "sidebar-item-link-for-some-extension-name-some-parent-id",
); );
fireEvent.click(parentLink); fireEvent.click(parentLink);
@ -245,25 +244,25 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
it("parent sidebar item is not highlighted", () => { it("parent sidebar item is not highlighted", () => {
const parent = getSidebarItem( const parent = getSidebarItem(
rendered, rendered,
"some-extension-id-some-parent-id", "some-extension-name-some-parent-id",
); );
expect(parent.dataset.isActiveTest).toBe("false"); expect(parent?.dataset.isActiveTest).toBe("false");
}); });
it("parent sidebar item is expanded", () => { it("parent sidebar item is expanded", () => {
const child = getSidebarItem( const child = getSidebarItem(
rendered, rendered,
"some-extension-id-some-child-id", "some-extension-name-some-child-id",
); );
expect(child).not.toBe(null); expect(child).not.toBeUndefined();
}); });
describe("when a child of the parent is selected", () => { describe("when a child of the parent is selected", () => {
beforeEach(() => { beforeEach(() => {
const childLink = rendered.getByTestId( const childLink = rendered.getByTestId(
"sidebar-item-link-for-some-extension-id-some-child-id", "sidebar-item-link-for-some-extension-name-some-child-id",
); );
fireEvent.click(childLink); fireEvent.click(childLink);
@ -276,19 +275,19 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
it("parent is highlighted", () => { it("parent is highlighted", () => {
const parent = getSidebarItem( const parent = getSidebarItem(
rendered, rendered,
"some-extension-id-some-parent-id", "some-extension-name-some-parent-id",
); );
expect(parent.dataset.isActiveTest).toBe("true"); expect(parent?.dataset.isActiveTest).toBe("true");
}); });
it("child is highlighted", () => { it("child is highlighted", () => {
const child = getSidebarItem( const child = getSidebarItem(
rendered, rendered,
"some-extension-id-some-child-id", "some-extension-name-some-child-id",
); );
expect(child.dataset.isActiveTest).toBe("true"); expect(child?.dataset.isActiveTest).toBe("true");
}); });
it("child page is shown", () => { it("child page is shown", () => {
@ -301,7 +300,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
it("tab for child page is active", () => { it("tab for child page is active", () => {
const tabLink = rendered.getByTestId( const tabLink = rendered.getByTestId(
"tab-link-for-some-extension-id-some-child-id", "tab-link-for-some-extension-name-some-child-id",
); );
expect(tabLink.dataset.isActiveTest).toBe("true"); expect(tabLink.dataset.isActiveTest).toBe("true");
@ -309,7 +308,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
it("tab for sibling page is not active", () => { it("tab for sibling page is not active", () => {
const tabLink = rendered.getByTestId( const tabLink = rendered.getByTestId(
"tab-link-for-some-extension-id-some-other-child-id", "tab-link-for-some-extension-name-some-other-child-id",
); );
expect(tabLink.dataset.isActiveTest).toBe("false"); expect(tabLink.dataset.isActiveTest).toBe("false");
@ -338,7 +337,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
expect(actual).toEqual({ expect(actual).toEqual({
sidebar: { sidebar: {
expanded: { "some-extension-id-some-parent-id": true }, expanded: { "some-extension-name-some-parent-id": true },
width: 200, width: 200,
}, },
}); });
@ -347,7 +346,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
describe("when selecting sibling tab", () => { describe("when selecting sibling tab", () => {
beforeEach(() => { beforeEach(() => {
const childTabLink = rendered.getByTestId( const childTabLink = rendered.getByTestId(
"tab-link-for-some-extension-id-some-other-child-id", "tab-link-for-some-extension-name-some-other-child-id",
); );
fireEvent.click(childTabLink); fireEvent.click(childTabLink);
@ -365,7 +364,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
it("tab for sibling page is active", () => { it("tab for sibling page is active", () => {
const tabLink = rendered.getByTestId( const tabLink = rendered.getByTestId(
"tab-link-for-some-extension-id-some-other-child-id", "tab-link-for-some-extension-name-some-other-child-id",
); );
expect(tabLink.dataset.isActiveTest).toBe("true"); expect(tabLink.dataset.isActiveTest).toBe("true");
@ -373,7 +372,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
it("tab for previous page is not active", () => { it("tab for previous page is not active", () => {
const tabLink = rendered.getByTestId( const tabLink = rendered.getByTestId(
"tab-link-for-some-extension-id-some-child-id", "tab-link-for-some-extension-name-some-child-id",
); );
expect(tabLink.dataset.isActiveTest).toBe("false"); expect(tabLink.dataset.isActiveTest).toBe("false");
@ -385,9 +384,9 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
}); });
}); });
const extensionStubWithSidebarItems: Partial<LensRendererExtension> = { const extensionStubWithSidebarItems: FakeExtensionData = {
id: "some-extension-id", id: "some-extension-id",
name: "some-extension-name",
clusterPages: [ clusterPages: [
{ {
components: { components: {
@ -396,7 +395,6 @@ const extensionStubWithSidebarItems: Partial<LensRendererExtension> = {
}, },
}, },
}, },
{ {
id: "some-child-page-id", id: "some-child-page-id",
@ -404,7 +402,6 @@ const extensionStubWithSidebarItems: Partial<LensRendererExtension> = {
Page: () => <div data-testid="some-child-page">Some child page</div>, Page: () => <div data-testid="some-child-page">Some child page</div>,
}, },
}, },
{ {
id: "some-other-child-page-id", id: "some-other-child-page-id",
@ -415,7 +412,6 @@ const extensionStubWithSidebarItems: Partial<LensRendererExtension> = {
}, },
}, },
], ],
clusterPageMenus: [ clusterPageMenus: [
{ {
id: "some-parent-id", id: "some-parent-id",
@ -433,7 +429,7 @@ const extensionStubWithSidebarItems: Partial<LensRendererExtension> = {
title: "Child 1", title: "Child 1",
components: { components: {
Icon: null, Icon: null as never,
}, },
}, },
@ -444,13 +440,8 @@ const extensionStubWithSidebarItems: Partial<LensRendererExtension> = {
title: "Child 2", title: "Child 2",
components: { components: {
Icon: null, Icon: null as never,
}, },
}, },
], ],
}; };
const getSidebarItem = (rendered: RenderResult, itemId: string) =>
rendered
.queryAllByTestId("sidebar-item")
.find((x) => x.dataset.idTest === itemId) || null;

View File

@ -14,6 +14,7 @@ import { routeInjectionToken } from "../../common/front-end-routing/route-inject
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token"; import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token";
import { getSidebarItem } from "../utils";
describe("cluster - visibility of sidebar items", () => { describe("cluster - visibility of sidebar items", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -43,7 +44,7 @@ describe("cluster - visibility of sidebar items", () => {
it("related sidebar item does not exist", () => { it("related sidebar item does not exist", () => {
const item = getSidebarItem(rendered, "some-item-id"); const item = getSidebarItem(rendered, "some-item-id");
expect(item).toBeNull(); expect(item).toBeUndefined();
}); });
describe("when kube resource becomes allowed", () => { describe("when kube resource becomes allowed", () => {
@ -58,17 +59,12 @@ describe("cluster - visibility of sidebar items", () => {
it("related sidebar item exists", () => { it("related sidebar item exists", () => {
const item = getSidebarItem(rendered, "some-item-id"); const item = getSidebarItem(rendered, "some-item-id");
expect(item).not.toBeNull(); expect(item).not.toBeUndefined();
}); });
}); });
}); });
}); });
const getSidebarItem = (rendered: RenderResult, itemId: string) =>
rendered
.queryAllByTestId("sidebar-item")
.find((x) => x.dataset.idTest === itemId) || null;
const testRouteInjectable = getInjectable({ const testRouteInjectable = getInjectable({
id: "some-route-injectable-id", id: "some-route-injectable-id",

View File

@ -2,12 +2,11 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { TestExtension } from "../renderer/components/test-utils/get-renderer-extension-fake"; import type { FakeExtensionData, TestExtension } from "../renderer/components/test-utils/get-renderer-extension-fake";
import { getRendererExtensionFake } from "../renderer/components/test-utils/get-renderer-extension-fake"; import { getRendererExtensionFakeFor } from "../renderer/components/test-utils/get-renderer-extension-fake";
import React from "react"; import React from "react";
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import currentPathInjectable from "../renderer/routes/current-path.injectable"; import currentPathInjectable from "../renderer/routes/current-path.injectable";
import type { LensRendererExtension } from "../extensions/lens-renderer-extension";
import type { ApplicationBuilder } from "../renderer/components/test-utils/get-application-builder"; import type { ApplicationBuilder } from "../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../renderer/components/test-utils/get-application-builder";
@ -18,6 +17,7 @@ describe("extension special characters in page registrations", () => {
beforeEach(async () => { beforeEach(async () => {
applicationBuilder = getApplicationBuilder(); applicationBuilder = getApplicationBuilder();
const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder);
testExtension = getRendererExtensionFake( testExtension = getRendererExtensionFake(
extensionWithPagesHavingSpecialCharacters, extensionWithPagesHavingSpecialCharacters,
@ -44,14 +44,14 @@ describe("extension special characters in page registrations", () => {
it("knows URL", () => { it("knows URL", () => {
const currentPath = applicationBuilder.dis.rendererDi.inject(currentPathInjectable); const currentPath = applicationBuilder.dis.rendererDi.inject(currentPathInjectable);
expect(currentPath.get()).toBe("/extension/some-extension-id--/some-page-id"); expect(currentPath.get()).toBe("/extension/some-extension-name--/some-page-id");
}); });
}); });
}); });
const extensionWithPagesHavingSpecialCharacters: Partial<LensRendererExtension> = { const extensionWithPagesHavingSpecialCharacters: FakeExtensionData = {
id: "@some-extension-id/", id: "some-extension-id",
name: "@some-extension-name/",
globalPages: [ globalPages: [
{ {
id: "/some-page-id/", id: "/some-page-id/",

View File

@ -23,9 +23,7 @@ exports[`extensions - navigation using application menu when navigating to exten
class="notice mb-14 mt-3" class="notice mb-14 mt-3"
> >
<p> <p>
Add new features via Lens Extensions. Add new features via Lens Extensions. Check out the
Check out
<a <a
href="https://docs.k8slens.dev/main//extensions/" href="https://docs.k8slens.dev/main//extensions/"
rel="noreferrer" rel="noreferrer"
@ -33,8 +31,7 @@ exports[`extensions - navigation using application menu when navigating to exten
> >
docs docs
</a> </a>
and list of
and list of
<a <a
href="https://github.com/lensapp/lens-extensions/blob/main/README.md" href="https://github.com/lensapp/lens-extensions/blob/main/README.md"
rel="noreferrer" rel="noreferrer"

View File

@ -9,8 +9,8 @@ import { getApplicationBuilder } from "../../renderer/components/test-utils/get-
import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable"; import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable";
import extensionsStoreInjectable from "../../extensions/extensions-store/extensions-store.injectable"; import extensionsStoreInjectable from "../../extensions/extensions-store/extensions-store.injectable";
import type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store"; import type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store";
import fileSystemProvisionerStoreInjectable from "../../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable"; import fileSystemProvisionerStoreInjectable from "../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable";
import type { FileSystemProvisionerStore } from "../../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store"; import type { FileSystemProvisionerStore } from "../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store";
import focusWindowInjectable from "../../renderer/ipc-channel-listeners/focus-window.injectable"; import focusWindowInjectable from "../../renderer/ipc-channel-listeners/focus-window.injectable";
// TODO: Make components free of side effects by making them deterministic // TODO: Make components free of side effects by making them deterministic

View File

@ -2,8 +2,8 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { TestExtension } from "../renderer/components/test-utils/get-renderer-extension-fake"; import type { FakeExtensionData, TestExtension } from "../renderer/components/test-utils/get-renderer-extension-fake";
import { getRendererExtensionFake } from "../renderer/components/test-utils/get-renderer-extension-fake"; import { getRendererExtensionFakeFor } from "../renderer/components/test-utils/get-renderer-extension-fake";
import React from "react"; import React from "react";
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react"; import { fireEvent } from "@testing-library/react";
@ -11,7 +11,6 @@ import isEmpty from "lodash/isEmpty";
import queryParametersInjectable from "../renderer/routes/query-parameters.injectable"; import queryParametersInjectable from "../renderer/routes/query-parameters.injectable";
import currentPathInjectable from "../renderer/routes/current-path.injectable"; import currentPathInjectable from "../renderer/routes/current-path.injectable";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import type { LensRendererExtension } from "../extensions/lens-renderer-extension";
import { getApplicationBuilder } from "../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../renderer/components/test-utils/get-application-builder";
describe("navigate to extension page", () => { describe("navigate to extension page", () => {
@ -22,6 +21,7 @@ describe("navigate to extension page", () => {
beforeEach(async () => { beforeEach(async () => {
const applicationBuilder = getApplicationBuilder(); const applicationBuilder = getApplicationBuilder();
const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder);
testExtension = getRendererExtensionFake( testExtension = getRendererExtensionFake(
extensionWithPagesHavingParameters, extensionWithPagesHavingParameters,
@ -51,7 +51,7 @@ describe("navigate to extension page", () => {
}); });
it("URL is correct", () => { it("URL is correct", () => {
expect(currentPath.get()).toBe("/extension/some-extension-id"); expect(currentPath.get()).toBe("/extension/some-extension-name");
}); });
it("query parameters is empty", () => { it("query parameters is empty", () => {
@ -70,7 +70,7 @@ describe("navigate to extension page", () => {
}); });
it("URL is correct", () => { it("URL is correct", () => {
expect(currentPath.get()).toBe("/extension/some-extension-id"); expect(currentPath.get()).toBe("/extension/some-extension-name");
}); });
it("knows query parameters", () => { it("knows query parameters", () => {
@ -98,7 +98,7 @@ describe("navigate to extension page", () => {
}); });
it("URL is correct", () => { it("URL is correct", () => {
expect(currentPath.get()).toBe("/extension/some-extension-id"); expect(currentPath.get()).toBe("/extension/some-extension-name");
}); });
it("knows query parameters", () => { it("knows query parameters", () => {
@ -120,14 +120,14 @@ describe("navigate to extension page", () => {
}); });
it("URL is correct", () => { it("URL is correct", () => {
expect(currentPath.get()).toBe("/extension/some-extension-id/some-child-page-id"); expect(currentPath.get()).toBe("/extension/some-extension-name/some-child-page-id");
}); });
}); });
}); });
const extensionWithPagesHavingParameters: Partial<LensRendererExtension> = { const extensionWithPagesHavingParameters: FakeExtensionData = {
id: "some-extension-id", id: "some-extension-id",
name: "some-extension-name",
globalPages: [ globalPages: [
{ {
components: { components: {
@ -159,20 +159,14 @@ const extensionWithPagesHavingParameters: Partial<LensRendererExtension> = {
params: { params: {
someStringParameter: "some-string-value", someStringParameter: "some-string-value",
someNumberParameter: { someNumberParameter: {
defaultValue: 42, defaultValue: 42,
stringify: (value) => value.toString(), stringify: (value) => value.toString(),
parse: (value) => (value ? Number(value) : undefined), parse: (value) => (value ? Number(value) : undefined),
}, },
someArrayParameter: { someArrayParameter: {
defaultValue: ["some-array-value", "some-other-array-value"], defaultValue: ["some-array-value", "some-other-array-value"],
stringify: (value) => value.join(","), stringify: (value) => value.join(","),
parse: (value: string[]) => (!isEmpty(value) ? value : undefined), parse: (value: string[]) => (!isEmpty(value) ? value : undefined),
}, },
}, },

View File

@ -124,7 +124,7 @@ exports[`preferences - closing-preferences given accessing preferences directly
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-26-live-region" id="react-select-theme-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -140,7 +140,7 @@ exports[`preferences - closing-preferences given accessing preferences directly
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-26-placeholder" id="react-select-theme-input-placeholder"
> >
Select... Select...
</div> </div>
@ -150,7 +150,7 @@ exports[`preferences - closing-preferences given accessing preferences directly
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-26-placeholder" aria-describedby="react-select-theme-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -209,7 +209,7 @@ exports[`preferences - closing-preferences given accessing preferences directly
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-27-live-region" id="react-select-extension-install-registry-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -225,7 +225,7 @@ exports[`preferences - closing-preferences given accessing preferences directly
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-27-placeholder" id="react-select-extension-install-registry-input-placeholder"
> >
Select... Select...
</div> </div>
@ -235,7 +235,7 @@ exports[`preferences - closing-preferences given accessing preferences directly
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-27-placeholder" aria-describedby="react-select-extension-install-registry-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -281,17 +281,12 @@ exports[`preferences - closing-preferences given accessing preferences directly
<p <p
class="mt-4 mb-5 leading-relaxed" class="mt-4 mb-5 leading-relaxed"
> >
This setting is to change the registry URL for installing extensions by name. This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
If you are unable to access the default registry (
https://registry.npmjs.org
)
you can change it in your
<b> <b>
.npmrc .npmrc
</b> </b>
 file or in the input below. file or in the input below.
</p> </p>
<div <div
class="Input theme round black disabled invalid" class="Input theme round black disabled invalid"
@ -349,7 +344,7 @@ exports[`preferences - closing-preferences given accessing preferences directly
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-28-live-region" id="react-select-update-channel-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -365,7 +360,7 @@ exports[`preferences - closing-preferences given accessing preferences directly
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-28-placeholder" id="react-select-update-channel-input-placeholder"
> >
Select... Select...
</div> </div>
@ -375,7 +370,7 @@ exports[`preferences - closing-preferences given accessing preferences directly
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-28-placeholder" aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -434,7 +429,7 @@ exports[`preferences - closing-preferences given accessing preferences directly
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-29-live-region" id="react-select-timezone-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -450,7 +445,7 @@ exports[`preferences - closing-preferences given accessing preferences directly
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-29-placeholder" id="react-select-timezone-input-placeholder"
> >
Select... Select...
</div> </div>
@ -460,7 +455,7 @@ exports[`preferences - closing-preferences given accessing preferences directly
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-29-placeholder" aria-describedby="react-select-timezone-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -827,7 +822,7 @@ exports[`preferences - closing-preferences given already in a page and then navi
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region" id="react-select-theme-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -843,7 +838,7 @@ exports[`preferences - closing-preferences given already in a page and then navi
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-2-placeholder" id="react-select-theme-input-placeholder"
> >
Select... Select...
</div> </div>
@ -853,7 +848,7 @@ exports[`preferences - closing-preferences given already in a page and then navi
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-2-placeholder" aria-describedby="react-select-theme-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -912,7 +907,7 @@ exports[`preferences - closing-preferences given already in a page and then navi
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region" id="react-select-extension-install-registry-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -928,7 +923,7 @@ exports[`preferences - closing-preferences given already in a page and then navi
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-3-placeholder" id="react-select-extension-install-registry-input-placeholder"
> >
Select... Select...
</div> </div>
@ -938,7 +933,7 @@ exports[`preferences - closing-preferences given already in a page and then navi
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-3-placeholder" aria-describedby="react-select-extension-install-registry-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -984,17 +979,12 @@ exports[`preferences - closing-preferences given already in a page and then navi
<p <p
class="mt-4 mb-5 leading-relaxed" class="mt-4 mb-5 leading-relaxed"
> >
This setting is to change the registry URL for installing extensions by name. This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
If you are unable to access the default registry (
https://registry.npmjs.org
)
you can change it in your
<b> <b>
.npmrc .npmrc
</b> </b>
 file or in the input below. file or in the input below.
</p> </p>
<div <div
class="Input theme round black disabled invalid" class="Input theme round black disabled invalid"
@ -1052,7 +1042,7 @@ exports[`preferences - closing-preferences given already in a page and then navi
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-4-live-region" id="react-select-update-channel-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -1068,7 +1058,7 @@ exports[`preferences - closing-preferences given already in a page and then navi
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-4-placeholder" id="react-select-update-channel-input-placeholder"
> >
Select... Select...
</div> </div>
@ -1078,7 +1068,7 @@ exports[`preferences - closing-preferences given already in a page and then navi
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-4-placeholder" aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -1137,7 +1127,7 @@ exports[`preferences - closing-preferences given already in a page and then navi
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-5-live-region" id="react-select-timezone-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -1153,7 +1143,7 @@ exports[`preferences - closing-preferences given already in a page and then navi
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-5-placeholder" id="react-select-timezone-input-placeholder"
> >
Select... Select...
</div> </div>
@ -1163,7 +1153,7 @@ exports[`preferences - closing-preferences given already in a page and then navi
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-5-placeholder" aria-describedby="react-select-timezone-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"

View File

@ -314,7 +314,7 @@ exports[`preferences - navigation to application preferences given in some child
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region" id="react-select-theme-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -330,7 +330,7 @@ exports[`preferences - navigation to application preferences given in some child
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-2-placeholder" id="react-select-theme-input-placeholder"
> >
Select... Select...
</div> </div>
@ -340,7 +340,7 @@ exports[`preferences - navigation to application preferences given in some child
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-2-placeholder" aria-describedby="react-select-theme-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -399,7 +399,7 @@ exports[`preferences - navigation to application preferences given in some child
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region" id="react-select-extension-install-registry-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -415,7 +415,7 @@ exports[`preferences - navigation to application preferences given in some child
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-3-placeholder" id="react-select-extension-install-registry-input-placeholder"
> >
Select... Select...
</div> </div>
@ -425,7 +425,7 @@ exports[`preferences - navigation to application preferences given in some child
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-3-placeholder" aria-describedby="react-select-extension-install-registry-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -471,17 +471,12 @@ exports[`preferences - navigation to application preferences given in some child
<p <p
class="mt-4 mb-5 leading-relaxed" class="mt-4 mb-5 leading-relaxed"
> >
This setting is to change the registry URL for installing extensions by name. This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
If you are unable to access the default registry (
https://registry.npmjs.org
)
you can change it in your
<b> <b>
.npmrc .npmrc
</b> </b>
 file or in the input below. file or in the input below.
</p> </p>
<div <div
class="Input theme round black disabled invalid" class="Input theme round black disabled invalid"
@ -539,7 +534,7 @@ exports[`preferences - navigation to application preferences given in some child
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-4-live-region" id="react-select-update-channel-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -555,7 +550,7 @@ exports[`preferences - navigation to application preferences given in some child
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-4-placeholder" id="react-select-update-channel-input-placeholder"
> >
Select... Select...
</div> </div>
@ -565,7 +560,7 @@ exports[`preferences - navigation to application preferences given in some child
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-4-placeholder" aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -624,7 +619,7 @@ exports[`preferences - navigation to application preferences given in some child
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-5-live-region" id="react-select-timezone-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -640,7 +635,7 @@ exports[`preferences - navigation to application preferences given in some child
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-5-placeholder" id="react-select-timezone-input-placeholder"
> >
Select... Select...
</div> </div>
@ -650,7 +645,7 @@ exports[`preferences - navigation to application preferences given in some child
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-5-placeholder" aria-describedby="react-select-timezone-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"

View File

@ -112,7 +112,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region" id="react-select-theme-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -128,7 +128,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-2-placeholder" id="react-select-theme-input-placeholder"
> >
Select... Select...
</div> </div>
@ -138,7 +138,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-2-placeholder" aria-describedby="react-select-theme-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -197,7 +197,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region" id="react-select-extension-install-registry-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -213,7 +213,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-3-placeholder" id="react-select-extension-install-registry-input-placeholder"
> >
Select... Select...
</div> </div>
@ -223,7 +223,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-3-placeholder" aria-describedby="react-select-extension-install-registry-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -269,17 +269,12 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
<p <p
class="mt-4 mb-5 leading-relaxed" class="mt-4 mb-5 leading-relaxed"
> >
This setting is to change the registry URL for installing extensions by name. This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
If you are unable to access the default registry (
https://registry.npmjs.org
)
you can change it in your
<b> <b>
.npmrc .npmrc
</b> </b>
 file or in the input below. file or in the input below.
</p> </p>
<div <div
class="Input theme round black disabled invalid" class="Input theme round black disabled invalid"
@ -337,7 +332,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-4-live-region" id="react-select-update-channel-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -353,7 +348,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-4-placeholder" id="react-select-update-channel-input-placeholder"
> >
Select... Select...
</div> </div>
@ -363,7 +358,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-4-placeholder" aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -422,7 +417,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-5-live-region" id="react-select-timezone-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -438,7 +433,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-5-placeholder" id="react-select-timezone-input-placeholder"
> >
Select... Select...
</div> </div>
@ -448,7 +443,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-5-placeholder" aria-describedby="react-select-timezone-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -668,7 +663,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-14-live-region" id="react-select-minimap-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -684,7 +679,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-14-placeholder" id="react-select-minimap-input-placeholder"
> >
Select... Select...
</div> </div>
@ -694,7 +689,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-14-placeholder" aria-describedby="react-select-minimap-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -752,7 +747,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-15-live-region" id="react-select-editor-line-numbers-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -768,7 +763,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-15-placeholder" id="react-select-editor-line-numbers-input-placeholder"
> >
Select... Select...
</div> </div>
@ -778,7 +773,7 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-15-placeholder" aria-describedby="react-select-editor-line-numbers-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"

View File

@ -112,7 +112,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region" id="react-select-theme-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -128,7 +128,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-2-placeholder" id="react-select-theme-input-placeholder"
> >
Select... Select...
</div> </div>
@ -138,7 +138,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-2-placeholder" aria-describedby="react-select-theme-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -197,7 +197,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region" id="react-select-extension-install-registry-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -213,7 +213,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-3-placeholder" id="react-select-extension-install-registry-input-placeholder"
> >
Select... Select...
</div> </div>
@ -223,7 +223,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-3-placeholder" aria-describedby="react-select-extension-install-registry-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -269,17 +269,12 @@ exports[`preferences - navigation to extension specific preferences given in pre
<p <p
class="mt-4 mb-5 leading-relaxed" class="mt-4 mb-5 leading-relaxed"
> >
This setting is to change the registry URL for installing extensions by name. This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
If you are unable to access the default registry (
https://registry.npmjs.org
)
you can change it in your
<b> <b>
.npmrc .npmrc
</b> </b>
 file or in the input below. file or in the input below.
</p> </p>
<div <div
class="Input theme round black disabled invalid" class="Input theme round black disabled invalid"
@ -337,7 +332,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-4-live-region" id="react-select-update-channel-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -353,7 +348,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-4-placeholder" id="react-select-update-channel-input-placeholder"
> >
Select... Select...
</div> </div>
@ -363,7 +358,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-4-placeholder" aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -422,7 +417,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-5-live-region" id="react-select-timezone-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -438,7 +433,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-5-placeholder" id="react-select-timezone-input-placeholder"
> >
Select... Select...
</div> </div>
@ -448,7 +443,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-5-placeholder" aria-describedby="react-select-timezone-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -657,7 +652,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-14-live-region" id="react-select-theme-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -673,7 +668,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-14-placeholder" id="react-select-theme-input-placeholder"
> >
Select... Select...
</div> </div>
@ -683,7 +678,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-14-placeholder" aria-describedby="react-select-theme-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -742,7 +737,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-15-live-region" id="react-select-extension-install-registry-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -758,7 +753,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-15-placeholder" id="react-select-extension-install-registry-input-placeholder"
> >
Select... Select...
</div> </div>
@ -768,7 +763,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-15-placeholder" aria-describedby="react-select-extension-install-registry-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -814,17 +809,12 @@ exports[`preferences - navigation to extension specific preferences given in pre
<p <p
class="mt-4 mb-5 leading-relaxed" class="mt-4 mb-5 leading-relaxed"
> >
This setting is to change the registry URL for installing extensions by name. This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
If you are unable to access the default registry (
https://registry.npmjs.org
)
you can change it in your
<b> <b>
.npmrc .npmrc
</b> </b>
 file or in the input below. file or in the input below.
</p> </p>
<div <div
class="Input theme round black disabled invalid" class="Input theme round black disabled invalid"
@ -882,7 +872,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-16-live-region" id="react-select-update-channel-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -898,7 +888,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-16-placeholder" id="react-select-update-channel-input-placeholder"
> >
Select... Select...
</div> </div>
@ -908,7 +898,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-16-placeholder" aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -967,7 +957,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-17-live-region" id="react-select-timezone-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -983,7 +973,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-17-placeholder" id="react-select-timezone-input-placeholder"
> >
Select... Select...
</div> </div>
@ -993,7 +983,7 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-17-placeholder" aria-describedby="react-select-timezone-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"

View File

@ -112,7 +112,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region" id="react-select-theme-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -128,7 +128,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-2-placeholder" id="react-select-theme-input-placeholder"
> >
Select... Select...
</div> </div>
@ -138,7 +138,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-2-placeholder" aria-describedby="react-select-theme-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -197,7 +197,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region" id="react-select-extension-install-registry-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -213,7 +213,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-3-placeholder" id="react-select-extension-install-registry-input-placeholder"
> >
Select... Select...
</div> </div>
@ -223,7 +223,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-3-placeholder" aria-describedby="react-select-extension-install-registry-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -269,17 +269,12 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
<p <p
class="mt-4 mb-5 leading-relaxed" class="mt-4 mb-5 leading-relaxed"
> >
This setting is to change the registry URL for installing extensions by name. This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
If you are unable to access the default registry (
https://registry.npmjs.org
)
you can change it in your
<b> <b>
.npmrc .npmrc
</b> </b>
 file or in the input below. file or in the input below.
</p> </p>
<div <div
class="Input theme round black disabled invalid" class="Input theme round black disabled invalid"
@ -337,7 +332,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-4-live-region" id="react-select-update-channel-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -353,7 +348,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-4-placeholder" id="react-select-update-channel-input-placeholder"
> >
Select... Select...
</div> </div>
@ -363,7 +358,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-4-placeholder" aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -422,7 +417,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-5-live-region" id="react-select-timezone-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -438,7 +433,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-5-placeholder" id="react-select-timezone-input-placeholder"
> >
Select... Select...
</div> </div>
@ -448,7 +443,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-5-placeholder" aria-describedby="react-select-timezone-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -664,7 +659,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-14-live-region" id="react-select-download-mirror-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -680,7 +675,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-14-placeholder" id="react-select-download-mirror-input-placeholder"
> >
Download mirror for kubectl Download mirror for kubectl
</div> </div>
@ -690,7 +685,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-14-placeholder" aria-describedby="react-select-download-mirror-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -845,7 +840,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-15-live-region" id="react-select-HelmRepoSelect-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -861,7 +856,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-15-placeholder" id="react-select-HelmRepoSelect-placeholder"
> >
Repositories Repositories
</div> </div>
@ -871,7 +866,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-15-placeholder" aria-describedby="react-select-HelmRepoSelect-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"

View File

@ -112,7 +112,7 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region" id="react-select-theme-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -128,7 +128,7 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-2-placeholder" id="react-select-theme-input-placeholder"
> >
Select... Select...
</div> </div>
@ -138,7 +138,7 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-2-placeholder" aria-describedby="react-select-theme-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -197,7 +197,7 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region" id="react-select-extension-install-registry-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -213,7 +213,7 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-3-placeholder" id="react-select-extension-install-registry-input-placeholder"
> >
Select... Select...
</div> </div>
@ -223,7 +223,7 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-3-placeholder" aria-describedby="react-select-extension-install-registry-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -269,17 +269,12 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
<p <p
class="mt-4 mb-5 leading-relaxed" class="mt-4 mb-5 leading-relaxed"
> >
This setting is to change the registry URL for installing extensions by name. This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
If you are unable to access the default registry (
https://registry.npmjs.org
)
you can change it in your
<b> <b>
.npmrc .npmrc
</b> </b>
 file or in the input below. file or in the input below.
</p> </p>
<div <div
class="Input theme round black disabled invalid" class="Input theme round black disabled invalid"
@ -337,7 +332,7 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-4-live-region" id="react-select-update-channel-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -353,7 +348,7 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-4-placeholder" id="react-select-update-channel-input-placeholder"
> >
Select... Select...
</div> </div>
@ -363,7 +358,7 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-4-placeholder" aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -422,7 +417,7 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-5-live-region" id="react-select-timezone-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -438,7 +433,7 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-5-placeholder" id="react-select-timezone-input-placeholder"
> >
Select... Select...
</div> </div>
@ -448,7 +443,7 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-5-placeholder" aria-describedby="react-select-timezone-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"

View File

@ -300,7 +300,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region" id="react-select-theme-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -316,7 +316,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-2-placeholder" id="react-select-theme-input-placeholder"
> >
Select... Select...
</div> </div>
@ -326,7 +326,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-2-placeholder" aria-describedby="react-select-theme-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -385,7 +385,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region" id="react-select-extension-install-registry-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -401,7 +401,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-3-placeholder" id="react-select-extension-install-registry-input-placeholder"
> >
Select... Select...
</div> </div>
@ -411,7 +411,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-3-placeholder" aria-describedby="react-select-extension-install-registry-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -457,17 +457,12 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
<p <p
class="mt-4 mb-5 leading-relaxed" class="mt-4 mb-5 leading-relaxed"
> >
This setting is to change the registry URL for installing extensions by name. This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
If you are unable to access the default registry (
https://registry.npmjs.org
)
you can change it in your
<b> <b>
.npmrc .npmrc
</b> </b>
 file or in the input below. file or in the input below.
</p> </p>
<div <div
class="Input theme round black disabled invalid" class="Input theme round black disabled invalid"
@ -525,7 +520,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-4-live-region" id="react-select-update-channel-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -541,7 +536,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-4-placeholder" id="react-select-update-channel-input-placeholder"
> >
Select... Select...
</div> </div>
@ -551,7 +546,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-4-placeholder" aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -610,7 +605,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-5-live-region" id="react-select-timezone-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -626,7 +621,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-5-placeholder" id="react-select-timezone-input-placeholder"
> >
Select... Select...
</div> </div>
@ -636,7 +631,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-5-placeholder" aria-describedby="react-select-timezone-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -845,7 +840,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-14-live-region" id="react-select-theme-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -861,7 +856,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-14-placeholder" id="react-select-theme-input-placeholder"
> >
Select... Select...
</div> </div>
@ -871,7 +866,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-14-placeholder" aria-describedby="react-select-theme-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -930,7 +925,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-15-live-region" id="react-select-extension-install-registry-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -946,7 +941,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-15-placeholder" id="react-select-extension-install-registry-input-placeholder"
> >
Select... Select...
</div> </div>
@ -956,7 +951,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-15-placeholder" aria-describedby="react-select-extension-install-registry-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -1002,17 +997,12 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
<p <p
class="mt-4 mb-5 leading-relaxed" class="mt-4 mb-5 leading-relaxed"
> >
This setting is to change the registry URL for installing extensions by name. This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
If you are unable to access the default registry (
https://registry.npmjs.org
)
you can change it in your
<b> <b>
.npmrc .npmrc
</b> </b>
 file or in the input below. file or in the input below.
</p> </p>
<div <div
class="Input theme round black disabled invalid" class="Input theme round black disabled invalid"
@ -1070,7 +1060,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-16-live-region" id="react-select-update-channel-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -1086,7 +1076,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-16-placeholder" id="react-select-update-channel-input-placeholder"
> >
Select... Select...
</div> </div>
@ -1096,7 +1086,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-16-placeholder" aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -1155,7 +1145,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-17-live-region" id="react-select-timezone-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -1171,7 +1161,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-17-placeholder" id="react-select-timezone-input-placeholder"
> >
Select... Select...
</div> </div>
@ -1181,7 +1171,7 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-17-placeholder" aria-describedby="react-select-timezone-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"

View File

@ -112,7 +112,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region" id="react-select-theme-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -128,7 +128,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-2-placeholder" id="react-select-theme-input-placeholder"
> >
Select... Select...
</div> </div>
@ -138,7 +138,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-2-placeholder" aria-describedby="react-select-theme-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -197,7 +197,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region" id="react-select-extension-install-registry-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -213,7 +213,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-3-placeholder" id="react-select-extension-install-registry-input-placeholder"
> >
Select... Select...
</div> </div>
@ -223,7 +223,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-3-placeholder" aria-describedby="react-select-extension-install-registry-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -269,17 +269,12 @@ exports[`preferences - navigation to terminal preferences given in preferences,
<p <p
class="mt-4 mb-5 leading-relaxed" class="mt-4 mb-5 leading-relaxed"
> >
This setting is to change the registry URL for installing extensions by name. This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
If you are unable to access the default registry (
https://registry.npmjs.org
)
you can change it in your
<b> <b>
.npmrc .npmrc
</b> </b>
 file or in the input below. file or in the input below.
</p> </p>
<div <div
class="Input theme round black disabled invalid" class="Input theme round black disabled invalid"
@ -337,7 +332,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-4-live-region" id="react-select-update-channel-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -353,7 +348,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-4-placeholder" id="react-select-update-channel-input-placeholder"
> >
Select... Select...
</div> </div>
@ -363,7 +358,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-4-placeholder" aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -422,7 +417,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-5-live-region" id="react-select-timezone-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -438,7 +433,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-5-placeholder" id="react-select-timezone-input-placeholder"
> >
Select... Select...
</div> </div>
@ -448,7 +443,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-5-placeholder" aria-describedby="react-select-timezone-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -689,7 +684,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-14-live-region" id="react-select-terminal-theme-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -705,7 +700,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-14-placeholder" id="react-select-terminal-theme-input-placeholder"
> >
Select... Select...
</div> </div>
@ -715,7 +710,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-14-placeholder" aria-describedby="react-select-terminal-theme-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"

View File

@ -114,7 +114,7 @@ exports[`preferences - navigation using application menu when navigating to pref
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region" id="react-select-theme-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -130,7 +130,7 @@ exports[`preferences - navigation using application menu when navigating to pref
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-2-placeholder" id="react-select-theme-input-placeholder"
> >
Select... Select...
</div> </div>
@ -140,7 +140,7 @@ exports[`preferences - navigation using application menu when navigating to pref
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-2-placeholder" aria-describedby="react-select-theme-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -199,7 +199,7 @@ exports[`preferences - navigation using application menu when navigating to pref
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region" id="react-select-extension-install-registry-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -215,7 +215,7 @@ exports[`preferences - navigation using application menu when navigating to pref
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-3-placeholder" id="react-select-extension-install-registry-input-placeholder"
> >
Select... Select...
</div> </div>
@ -225,7 +225,7 @@ exports[`preferences - navigation using application menu when navigating to pref
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-3-placeholder" aria-describedby="react-select-extension-install-registry-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -271,17 +271,12 @@ exports[`preferences - navigation using application menu when navigating to pref
<p <p
class="mt-4 mb-5 leading-relaxed" class="mt-4 mb-5 leading-relaxed"
> >
This setting is to change the registry URL for installing extensions by name. This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
If you are unable to access the default registry (
https://registry.npmjs.org
)
you can change it in your
<b> <b>
.npmrc .npmrc
</b> </b>
 file or in the input below. file or in the input below.
</p> </p>
<div <div
class="Input theme round black disabled invalid" class="Input theme round black disabled invalid"
@ -339,7 +334,7 @@ exports[`preferences - navigation using application menu when navigating to pref
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-4-live-region" id="react-select-update-channel-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -355,7 +350,7 @@ exports[`preferences - navigation using application menu when navigating to pref
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-4-placeholder" id="react-select-update-channel-input-placeholder"
> >
Select... Select...
</div> </div>
@ -365,7 +360,7 @@ exports[`preferences - navigation using application menu when navigating to pref
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-4-placeholder" aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -424,7 +419,7 @@ exports[`preferences - navigation using application menu when navigating to pref
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-5-live-region" id="react-select-timezone-input-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -440,7 +435,7 @@ exports[`preferences - navigation using application menu when navigating to pref
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-5-placeholder" id="react-select-timezone-input-placeholder"
> >
Select... Select...
</div> </div>
@ -450,7 +445,7 @@ exports[`preferences - navigation using application menu when navigating to pref
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-5-placeholder" aria-describedby="react-select-timezone-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"

View File

@ -12,8 +12,6 @@ import { routeInjectionToken } from "../../common/front-end-routing/route-inject
import { computed } from "mobx"; import { computed } from "mobx";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import { preferenceNavigationItemInjectionToken } from "../../renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable"; import { preferenceNavigationItemInjectionToken } from "../../renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../renderer/routes/route-is-active.injectable"; import routeIsActiveInjectable from "../../renderer/routes/route-is-active.injectable";
import { Preferences } from "../../renderer/components/+preferences"; import { Preferences } from "../../renderer/components/+preferences";
@ -26,6 +24,7 @@ import { createObservableHistory } from "mobx-observable-history";
import navigateToPreferenceTabInjectable from "../../renderer/components/+preferences/preferences-navigation/navigate-to-preference-tab.injectable"; import navigateToPreferenceTabInjectable from "../../renderer/components/+preferences/preferences-navigation/navigate-to-preference-tab.injectable";
import navigateToFrontPageInjectable from "../../common/front-end-routing/navigate-to-front-page.injectable"; import navigateToFrontPageInjectable from "../../common/front-end-routing/navigate-to-front-page.injectable";
import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token"; import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token";
import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
describe("preferences - closing-preferences", () => { describe("preferences - closing-preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -45,10 +44,10 @@ describe("preferences - closing-preferences", () => {
} as unknown as UserStore; } as unknown as UserStore;
rendererDi.override(userStoreInjectable, () => userStoreStub); rendererDi.override(userStoreInjectable, () => userStoreStub);
rendererDi.override(ipcRendererInjectable, () => ({
const themeStoreStub = { themeOptions: [] } as unknown as ThemeStore; on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
rendererDi.override(themeStoreInjectable, () => themeStoreStub); } as never));
rendererDi.override(navigateToFrontPageInjectable, (di) => { rendererDi.override(navigateToFrontPageInjectable, (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken); const navigateToRoute = di.inject(navigateToRouteInjectionToken);

View File

@ -7,10 +7,8 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable"; import navigateToProxyPreferencesInjectable from "../../common/front-end-routing/routes/preferences/proxy/navigate-to-proxy-preferences.injectable";
import type { ThemeStore } from "../../renderer/theme.store"; import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
import navigateToProxyPreferencesInjectable
from "../../common/front-end-routing/routes/preferences/proxy/navigate-to-proxy-preferences.injectable";
describe("preferences - navigation to application preferences", () => { describe("preferences - navigation to application preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -24,10 +22,10 @@ describe("preferences - navigation to application preferences", () => {
} as unknown as UserStore; } as unknown as UserStore;
rendererDi.override(userStoreInjectable, () => userStoreStub); rendererDi.override(userStoreInjectable, () => userStoreStub);
rendererDi.override(ipcRendererInjectable, () => ({
const themeStoreStub = ({ themeOptions: [] }) as unknown as ThemeStore; on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
rendererDi.override(themeStoreInjectable, () => themeStoreStub); } as never));
}); });
}); });

View File

@ -7,8 +7,7 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable"; import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
describe("preferences - navigation to editor preferences", () => { describe("preferences - navigation to editor preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -23,10 +22,10 @@ describe("preferences - navigation to editor preferences", () => {
} as unknown as UserStore; } as unknown as UserStore;
rendererDi.override(userStoreInjectable, () => userStoreStub); rendererDi.override(userStoreInjectable, () => userStoreStub);
rendererDi.override(ipcRendererInjectable, () => ({
const themeStoreStub = ({ themeOptions: [] }) as unknown as ThemeStore; on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
rendererDi.override(themeStoreInjectable, () => themeStoreStub); } as never));
}); });
}); });

View File

@ -7,11 +7,10 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import type { LensRendererExtension } from "../../extensions/lens-renderer-extension";
import React from "react"; import React from "react";
import { getRendererExtensionFake } from "../../renderer/components/test-utils/get-renderer-extension-fake"; import type { FakeExtensionData } from "../../renderer/components/test-utils/get-renderer-extension-fake";
import { getRendererExtensionFakeFor } from "../../renderer/components/test-utils/get-renderer-extension-fake";
import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
describe("preferences - navigation to extension specific preferences", () => { describe("preferences - navigation to extension specific preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -25,10 +24,10 @@ describe("preferences - navigation to extension specific preferences", () => {
} as unknown as UserStore; } as unknown as UserStore;
rendererDi.override(userStoreInjectable, () => userStoreStub); rendererDi.override(userStoreInjectable, () => userStoreStub);
rendererDi.override(ipcRendererInjectable, () => ({
const themeStoreStub = { themeOptions: [] } as unknown as ThemeStore; on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
rendererDi.override(themeStoreInjectable, () => themeStoreStub); } as never));
}); });
}); });
@ -61,6 +60,7 @@ describe("preferences - navigation to extension specific preferences", () => {
describe("when extension with specific preferences is enabled", () => { describe("when extension with specific preferences is enabled", () => {
beforeEach(() => { beforeEach(() => {
const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder);
const testExtension = getRendererExtensionFake(extensionStubWithExtensionSpecificPreferenceItems); const testExtension = getRendererExtensionFake(extensionStubWithExtensionSpecificPreferenceItems);
applicationBuilder.addExtensions(testExtension); applicationBuilder.addExtensions(testExtension);
@ -107,9 +107,9 @@ describe("preferences - navigation to extension specific preferences", () => {
}); });
}); });
const extensionStubWithExtensionSpecificPreferenceItems: Partial<LensRendererExtension> = { const extensionStubWithExtensionSpecificPreferenceItems: FakeExtensionData = {
id: "some-test-extension-id", id: "some-extension-id",
name: "some-extension-name",
appPreferences: [ appPreferences: [
{ {
title: "Some preference item", title: "Some preference item",

View File

@ -7,9 +7,8 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import { observable } from "mobx"; import { observable } from "mobx";
import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
describe("preferences - navigation to kubernetes preferences", () => { describe("preferences - navigation to kubernetes preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -24,10 +23,10 @@ describe("preferences - navigation to kubernetes preferences", () => {
} as unknown as UserStore; } as unknown as UserStore;
rendererDi.override(userStoreInjectable, () => userStoreStub); rendererDi.override(userStoreInjectable, () => userStoreStub);
rendererDi.override(ipcRendererInjectable, () => ({
const themeStoreStub = ({ themeOptions: [] }) as unknown as ThemeStore; on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
rendererDi.override(themeStoreInjectable, () => themeStoreStub); } as never));
}); });
}); });

View File

@ -7,8 +7,7 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable"; import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
describe("preferences - navigation to proxy preferences", () => { describe("preferences - navigation to proxy preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -22,10 +21,10 @@ describe("preferences - navigation to proxy preferences", () => {
} as unknown as UserStore; } as unknown as UserStore;
rendererDi.override(userStoreInjectable, () => userStoreStub); rendererDi.override(userStoreInjectable, () => userStoreStub);
rendererDi.override(ipcRendererInjectable, () => ({
const themeStoreStub = ({ themeOptions: [] }) as unknown as ThemeStore; on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
rendererDi.override(themeStoreInjectable, () => themeStoreStub); } as never));
}); });
}); });

View File

@ -6,13 +6,13 @@ import type { RenderResult } from "@testing-library/react";
import React from "react"; import React from "react";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getRendererExtensionFake } from "../../renderer/components/test-utils/get-renderer-extension-fake"; import type { FakeExtensionData } from "../../renderer/components/test-utils/get-renderer-extension-fake";
import { getRendererExtensionFakeFor } from "../../renderer/components/test-utils/get-renderer-extension-fake";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import navigateToTelemetryPreferencesInjectable from "../../common/front-end-routing/routes/preferences/telemetry/navigate-to-telemetry-preferences.injectable"; import navigateToTelemetryPreferencesInjectable from "../../common/front-end-routing/routes/preferences/telemetry/navigate-to-telemetry-preferences.injectable";
import sentryDnsUrlInjectable from "../../renderer/components/+preferences/sentry-dns-url.injectable"; import sentryDnsUrlInjectable from "../../renderer/components/+preferences/sentry-dns-url.injectable";
import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
describe("preferences - navigation to telemetry preferences", () => { describe("preferences - navigation to telemetry preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -26,10 +26,10 @@ describe("preferences - navigation to telemetry preferences", () => {
} as unknown as UserStore; } as unknown as UserStore;
rendererDi.override(userStoreInjectable, () => userStoreStub); rendererDi.override(userStoreInjectable, () => userStoreStub);
rendererDi.override(ipcRendererInjectable, () => ({
const themeStoreStub = { themeOptions: [] } as unknown as ThemeStore; on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
rendererDi.override(themeStoreInjectable, () => themeStoreStub); } as never));
}); });
}); });
@ -62,8 +62,8 @@ describe("preferences - navigation to telemetry preferences", () => {
describe("when extension with telemetry preference items gets enabled", () => { describe("when extension with telemetry preference items gets enabled", () => {
beforeEach(() => { beforeEach(() => {
const testExtensionWithTelemetryPreferenceItems = const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder);
getRendererExtensionFake(extensionStubWithTelemetryPreferenceItems); const testExtensionWithTelemetryPreferenceItems = getRendererExtensionFake(extensionStubWithTelemetryPreferenceItems);
applicationBuilder.addExtensions( applicationBuilder.addExtensions(
testExtensionWithTelemetryPreferenceItems, testExtensionWithTelemetryPreferenceItems,
@ -106,18 +106,19 @@ describe("preferences - navigation to telemetry preferences", () => {
}); });
it("given extensions but no telemetry preference items, does not show link for telemetry preferences", () => { it("given extensions but no telemetry preference items, does not show link for telemetry preferences", () => {
const testExtensionWithTelemetryPreferenceItems = const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder);
getRendererExtensionFake({ const testExtensionWithTelemetryPreferenceItems = getRendererExtensionFake({
id: "some-test-extension-id", id: "some-test-extension-id",
appPreferences: [ name: "some-test-extension-name",
{ appPreferences: [
title: "irrelevant", {
id: "irrelevant", title: "irrelevant",
showInPreferencesTab: "not-telemetry", id: "irrelevant",
components: { Hint: () => <div />, Input: () => <div /> }, showInPreferencesTab: "not-telemetry",
}, components: { Hint: () => <div />, Input: () => <div /> },
], },
}); ],
});
applicationBuilder.addExtensions( applicationBuilder.addExtensions(
testExtensionWithTelemetryPreferenceItems, testExtensionWithTelemetryPreferenceItems,
@ -186,8 +187,9 @@ describe("preferences - navigation to telemetry preferences", () => {
}); });
}); });
const extensionStubWithTelemetryPreferenceItems = { const extensionStubWithTelemetryPreferenceItems: FakeExtensionData = {
id: "some-test-extension-id", id: "some-test-extension-id",
name: "some-test-extension-name",
appPreferences: [ appPreferences: [
{ {
title: "Some telemetry-preference item", title: "Some telemetry-preference item",

View File

@ -7,10 +7,9 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import { observable } from "mobx"; import { observable } from "mobx";
import defaultShellInjectable from "../../renderer/components/+preferences/default-shell.injectable"; import defaultShellInjectable from "../../renderer/components/+preferences/default-shell.injectable";
import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
describe("preferences - navigation to terminal preferences", () => { describe("preferences - navigation to terminal preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -26,12 +25,11 @@ describe("preferences - navigation to terminal preferences", () => {
} as unknown as UserStore; } as unknown as UserStore;
rendererDi.override(userStoreInjectable, () => userStoreStub); rendererDi.override(userStoreInjectable, () => userStoreStub);
rendererDi.override(defaultShellInjectable, () => "some-default-shell"); rendererDi.override(defaultShellInjectable, () => "some-default-shell");
rendererDi.override(ipcRendererInjectable, () => ({
const themeStoreStub = ({ themeOptions: [] }) as unknown as ThemeStore; on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
rendererDi.override(themeStoreInjectable, () => themeStoreStub); } as never));
}); });
}); });

View File

@ -9,8 +9,7 @@ import { getApplicationBuilder } from "../../renderer/components/test-utils/get-
import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable"; import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store"; import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
describe("preferences - navigation using application menu", () => { describe("preferences - navigation using application menu", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -27,10 +26,10 @@ describe("preferences - navigation using application menu", () => {
} as unknown as UserStore; } as unknown as UserStore;
rendererDi.override(userStoreInjectable, () => userStoreStub); rendererDi.override(userStoreInjectable, () => userStoreStub);
rendererDi.override(ipcRendererInjectable, () => ({
const themeStoreStub = { themeOptions: [] } as unknown as ThemeStore; on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
rendererDi.override(themeStoreInjectable, () => themeStoreStub); } as never));
}); });
rendered = await applicationBuilder.render(); rendered = await applicationBuilder.render();

12
src/behaviours/utils.ts Normal file
View File

@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
export function getSidebarItem(rendered: RenderResult, itemId: string) {
return rendered
.queryAllByTestId("sidebar-item")
.find((x) => x.dataset.idTest === itemId);
}

View File

@ -27,16 +27,17 @@ exports[`welcome - navigation using application menu when navigating to welcome
style="width: 320px;" style="width: 320px;"
> >
<h2> <h2>
Welcome to Welcome to OpenLens 5!
OpenLens
5!
</h2> </h2>
<p> <p>
To get you started we have auto-detected your clusters in your kubeconfig file and added them to the catalog, your centralized view for managing all your cloud-native resources. To get you started we have auto-detected your clusters in your
<br />
<br />
If you have any questions or feedback, please join our
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our
<a <a
class="link" class="link"
href="https://join.slack.com/t/k8slens/shared_invite/zt-wcl8jq3k-68R5Wcmk1o95MLBE5igUDQ" href="https://join.slack.com/t/k8slens/shared_invite/zt-wcl8jq3k-68R5Wcmk1o95MLBE5igUDQ"

View File

@ -30,9 +30,9 @@ interface TestStoreModel {
} }
class TestStore extends BaseStore<TestStoreModel> { class TestStore extends BaseStore<TestStoreModel> {
@observable a: string; @observable a = "";
@observable b: string; @observable b = "";
@observable c: string; @observable c = "";
constructor() { constructor() {
super({ super({
@ -90,7 +90,6 @@ describe("BaseStore", () => {
await mainDi.runSetups(); await mainDi.runSetups();
store = undefined;
TestStore.resetInstance(); TestStore.resetInstance();
const mockOpts = { const mockOpts = {

View File

@ -14,16 +14,13 @@ import { stdout, stderr } from "process";
import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable"; import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
import type { ClusterModel } from "../cluster-types"; import type { ClusterModel } from "../cluster-types";
import type { import type { DiContainer } from "@ogre-tools/injectable";
DiContainer,
} from "@ogre-tools/injectable";
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token"; import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable"; import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable";
import assert from "assert";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -148,6 +145,8 @@ describe("cluster-store", () => {
it("adds new cluster to store", async () => { it("adds new cluster to store", async () => {
const storedCluster = clusterStore.getById("foo"); const storedCluster = clusterStore.getById("foo");
assert(storedCluster);
expect(storedCluster.id).toBe("foo"); expect(storedCluster.id).toBe("foo");
expect(storedCluster.preferences.terminalCWD).toBe("/some-directory-for-user-data"); expect(storedCluster.preferences.terminalCWD).toBe("/some-directory-for-user-data");
expect(storedCluster.preferences.icon).toBe( expect(storedCluster.preferences.icon).toBe(
@ -249,6 +248,8 @@ describe("cluster-store", () => {
it("allows to retrieve a cluster", () => { it("allows to retrieve a cluster", () => {
const storedCluster = clusterStore.getById("cluster1"); const storedCluster = clusterStore.getById("cluster1");
assert(storedCluster);
expect(storedCluster.id).toBe("cluster1"); expect(storedCluster.id).toBe("cluster1");
expect(storedCluster.preferences.terminalCWD).toBe("/foo"); expect(storedCluster.preferences.terminalCWD).toBe("/foo");
}); });
@ -379,6 +380,7 @@ users:
it("migrates to modern format with icon not in file", async () => { it("migrates to modern format with icon not in file", async () => {
const { icon } = clusterStore.clustersList[0].preferences; const { icon } = clusterStore.clustersList[0].preferences;
assert(icon);
expect(icon.startsWith("data:;base64,")).toBe(true); expect(icon.startsWith("data:;base64,")).toBe(true);
}); });
}); });

View File

@ -5,7 +5,7 @@
import type { AppEvent } from "../app-event-bus/event-bus"; import type { AppEvent } from "../app-event-bus/event-bus";
import { appEventBus } from "../app-event-bus/event-bus"; import { appEventBus } from "../app-event-bus/event-bus";
import { Console } from "console"; import { assert, Console } from "console";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -13,14 +13,15 @@ console = new Console(stdout, stderr);
describe("event bus tests", () => { describe("event bus tests", () => {
describe("emit", () => { describe("emit", () => {
it("emits an event", () => { it("emits an event", () => {
let event: AppEvent = null; let event: AppEvent | undefined;
appEventBus.addListener((data) => { appEventBus.addListener((data) => {
event = data; event = data;
}); });
appEventBus.emit({ name: "foo", action: "bar" }); appEventBus.emit({ name: "foo", action: "bar" });
expect(event.name).toBe("foo"); assert(event);
expect(event?.name).toBe("foo");
}); });
}); });
}); });

View File

@ -21,7 +21,7 @@ describe("EventEmitter", () => {
let called = false; let called = false;
const e = new EventEmitter<[]>(); const e = new EventEmitter<[]>();
e.addListener(() => 0 as any, {}); e.addListener(() => 0 as never, {});
e.addListener(() => { called = true; }, {}); e.addListener(() => { called = true; }, {});
e.emit(); e.emit();

View File

@ -5,69 +5,21 @@
import { anyObject } from "jest-mock-extended"; import { anyObject } from "jest-mock-extended";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import logger from "../../main/logger";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog"; import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable"; import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "../hotbar-store.injectable"; import hotbarStoreInjectable from "../hotbars/store.injectable";
import { HotbarStore } from "../hotbar-store"; import type { HotbarStore } from "../hotbars/store";
import catalogEntityRegistryInjectable from "../../main/catalog/entity-registry.injectable";
import { computed } from "mobx";
import hasCategoryForEntityInjectable from "../catalog/has-category-for-entity.injectable";
import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable"; import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import loggerInjectable from "../logger.injectable";
import type { Logger } from "../logger";
jest.mock("../../main/catalog/catalog-entity-registry", () => ({ console.log("I am here as reminder against mockfs (and to fix console logging)");
catalogEntityRegistry: {
items: [
getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "1dfa26e2ebab15780a3547e9c7fa785c",
name: "mycluster",
source: "local",
labels: {},
},
}),
getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "55b42c3c7ba3b04193416cda405269a5",
name: "my_shiny_cluster",
source: "remote",
labels: {},
},
}),
getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "catalog-entity",
name: "Catalog",
source: "app",
labels: {},
},
}),
],
},
}));
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity { function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
return { return {
@ -84,69 +36,91 @@ function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKi
} as CatalogEntity; } as CatalogEntity;
} }
const testCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "test",
name: "test",
labels: {},
},
});
const minikubeCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "minikube",
name: "minikube",
labels: {},
},
});
const awsCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "aws",
name: "aws",
labels: {},
},
});
describe("HotbarStore", () => { describe("HotbarStore", () => {
let di: DiContainer; let di: DiContainer;
let hotbarStore: HotbarStore; let hotbarStore: HotbarStore;
let testCluster: CatalogEntity;
let minikubeCluster: CatalogEntity;
let awsCluster: CatalogEntity;
let loggerMock: jest.Mocked<Logger>;
beforeEach(async () => { beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true }); di = getDiForUnitTesting({ doGeneralOverrides: true });
(di as any).unoverride(hotbarStoreInjectable);
testCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "some-test-id",
name: "my-test-cluster",
source: "local",
labels: {},
},
});
minikubeCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "some-minikube-id",
name: "my-minikube-cluster",
source: "local",
labels: {},
},
});
awsCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "some-aws-id",
name: "my-aws-cluster",
source: "local",
labels: {},
},
});
di.override(hasCategoryForEntityInjectable, () => () => true);
loggerMock = {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
silly: jest.fn(),
};
di.override(loggerInjectable, () => loggerMock);
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
catalogEntityRegistry.addComputedSource("some-id", computed(() => [
testCluster,
minikubeCluster,
awsCluster,
catalogCatalogEntity,
]));
di.permitSideEffects(getConfigurationFileModelInjectable); di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable); di.permitSideEffects(appVersionInjectable);
di.permitSideEffects(hotbarStoreInjectable);
di.override(hotbarStoreInjectable, () => {
HotbarStore.resetInstance();
return HotbarStore.createInstance({
catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable),
});
});
}); });
afterEach(() => { afterEach(() => {
mockFs.restore(); mockFs.restore();
}); });
describe("given no migrations", () => { describe("given no previous data in store, running all migrations", () => {
beforeEach(async () => { beforeEach(async () => {
mockFs(); mockFs();
@ -174,7 +148,7 @@ describe("HotbarStore", () => {
}); });
it("initially adds catalog entity as first item", () => { it("initially adds catalog entity as first item", () => {
expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog"); expect(hotbarStore.getActive().items[0]?.entity.name).toEqual("Catalog");
}); });
it("adds items", () => { it("adds items", () => {
@ -186,7 +160,7 @@ describe("HotbarStore", () => {
it("removes items", () => { it("removes items", () => {
hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("test"); hotbarStore.removeFromHotbar("some-test-id");
hotbarStore.removeFromHotbar("catalog-entity"); hotbarStore.removeFromHotbar("catalog-entity");
const items = hotbarStore.getActive().items.filter(Boolean); const items = hotbarStore.getActive().items.filter(Boolean);
@ -211,7 +185,7 @@ describe("HotbarStore", () => {
hotbarStore.restackItems(1, 5); hotbarStore.restackItems(1, 5);
expect(hotbarStore.getActive().items[5]).toBeTruthy(); expect(hotbarStore.getActive().items[5]).toBeTruthy();
expect(hotbarStore.getActive().items[5].entity.uid).toEqual("test"); expect(hotbarStore.getActive().items[5]?.entity.uid).toEqual("some-test-id");
}); });
it("moves items down", () => { it("moves items down", () => {
@ -224,7 +198,7 @@ describe("HotbarStore", () => {
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null); const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
expect(items.slice(0, 4)).toEqual(["aws", "catalog-entity", "test", "minikube"]); expect(items.slice(0, 4)).toEqual(["some-aws-id", "catalog-entity", "some-test-id", "some-minikube-id"]);
}); });
it("moves items up", () => { it("moves items up", () => {
@ -237,28 +211,21 @@ describe("HotbarStore", () => {
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null); const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
expect(items.slice(0, 4)).toEqual(["catalog-entity", "minikube", "aws", "test"]); expect(items.slice(0, 4)).toEqual(["catalog-entity", "some-minikube-id", "some-aws-id", "some-test-id"]);
}); });
it("logs an error if cellIndex is out of bounds", () => { it("logs an error if cellIndex is out of bounds", () => {
hotbarStore.add({ name: "hottest", id: "hottest" }); hotbarStore.add({ name: "hottest", id: "hottest" });
hotbarStore.setActiveHotbar("hottest"); hotbarStore.setActiveHotbar("hottest");
const { error } = logger;
const mocked = jest.fn();
logger.error = mocked;
hotbarStore.addToHotbar(testCluster, -1); hotbarStore.addToHotbar(testCluster, -1);
expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject()); expect(loggerMock.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
hotbarStore.addToHotbar(testCluster, 12); hotbarStore.addToHotbar(testCluster, 12);
expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject()); expect(loggerMock.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
hotbarStore.addToHotbar(testCluster, 13); hotbarStore.addToHotbar(testCluster, 13);
expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject()); expect(loggerMock.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
logger.error = error;
}); });
it("throws an error if getId is invalid or returns not a string", () => { it("throws an error if getId is invalid or returns not a string", () => {
@ -275,7 +242,7 @@ describe("HotbarStore", () => {
hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(testCluster);
hotbarStore.restackItems(1, 1); hotbarStore.restackItems(1, 1);
expect(hotbarStore.getActive().items[1].entity.uid).toEqual("test"); expect(hotbarStore.getActive().items[1]?.entity.uid).toEqual("some-test-id");
}); });
it("new items takes first empty cell", () => { it("new items takes first empty cell", () => {
@ -284,7 +251,7 @@ describe("HotbarStore", () => {
hotbarStore.restackItems(0, 3); hotbarStore.restackItems(0, 3);
hotbarStore.addToHotbar(minikubeCluster); hotbarStore.addToHotbar(minikubeCluster);
expect(hotbarStore.getActive().items[0].entity.uid).toEqual("minikube"); expect(hotbarStore.getActive().items[0]?.entity.uid).toEqual("some-minikube-id");
}); });
it("throws if invalid arguments provided", () => { it("throws if invalid arguments provided", () => {
@ -315,7 +282,7 @@ describe("HotbarStore", () => {
}); });
}); });
describe("given pre beta-5 configurations", () => { describe("given data from 5.0.0-beta.3 and version being 5.0.0-beta.10", () => {
beforeEach(async () => { beforeEach(async () => {
const configurationToBeMigrated = { const configurationToBeMigrated = {
"some-electron-app-path-for-user-data": { "some-electron-app-path-for-user-data": {
@ -332,7 +299,7 @@ describe("HotbarStore", () => {
items: [ items: [
{ {
entity: { entity: {
uid: "1dfa26e2ebab15780a3547e9c7fa785c", uid: "some-aws-id",
}, },
}, },
{ {
@ -381,15 +348,17 @@ describe("HotbarStore", () => {
mockFs(configurationToBeMigrated); mockFs(configurationToBeMigrated);
di.override(appVersionInjectable, () => "5.0.0-beta.10");
await di.runSetups(); await di.runSetups();
hotbarStore = di.inject(hotbarStoreInjectable); hotbarStore = di.inject(hotbarStoreInjectable);
}); });
it("allows to retrieve a hotbar", () => { it("allows to retrieve a hotbar", () => {
const hotbar = hotbarStore.getById("3caac17f-aec2-4723-9694-ad204465d935"); const hotbar = hotbarStore.findById("3caac17f-aec2-4723-9694-ad204465d935");
expect(hotbar.id).toBe("3caac17f-aec2-4723-9694-ad204465d935"); expect(hotbar?.id).toBe("3caac17f-aec2-4723-9694-ad204465d935");
}); });
it("clears cells without entity", () => { it("clears cells without entity", () => {
@ -403,17 +372,9 @@ describe("HotbarStore", () => {
expect(items[0]).toEqual({ expect(items[0]).toEqual({
entity: { entity: {
name: "mycluster", name: "my-aws-cluster",
source: "local", source: "local",
uid: "1dfa26e2ebab15780a3547e9c7fa785c", uid: "some-aws-id",
},
});
expect(items[1]).toEqual({
entity: {
name: "my_shiny_cluster",
source: "remote",
uid: "55b42c3c7ba3b04193416cda405269a5",
}, },
}); });
}); });

View File

@ -6,13 +6,14 @@ import https from "https";
import os from "os"; import os from "os";
import { getMacRootCA, getWinRootCA, injectCAs, DSTRootCAX3 } from "../system-ca"; import { getMacRootCA, getWinRootCA, injectCAs, DSTRootCAX3 } from "../system-ca";
import { dependencies, devDependencies } from "../../../package.json"; import { dependencies, devDependencies } from "../../../package.json";
import assert from "assert";
const deps = { ...dependencies, ...devDependencies }; const deps = { ...dependencies, ...devDependencies };
// Skip the test if mac-ca is not installed, or os is not darwin // Skip the test if mac-ca is not installed, or os is not darwin
(deps["mac-ca"] && os.platform().includes("darwin") ? describe: describe.skip)("inject CA for Mac", () => { (deps["mac-ca"] && os.platform().includes("darwin") ? describe: describe.skip)("inject CA for Mac", () => {
// for reset https.globalAgent.options.ca after testing // for reset https.globalAgent.options.ca after testing
let _ca: string | Buffer | (string | Buffer)[]; let _ca: string | Buffer | (string | Buffer)[] | undefined;
beforeEach(() => { beforeEach(() => {
_ca = https.globalAgent.options.ca; _ca = https.globalAgent.options.ca;
@ -44,6 +45,7 @@ const deps = { ...dependencies, ...devDependencies };
injectCAs(osxCAs); injectCAs(osxCAs);
const injected = https.globalAgent.options.ca; const injected = https.globalAgent.options.ca;
assert(injected);
expect(injected.includes(DSTRootCAX3)).toBeFalsy(); expect(injected.includes(DSTRootCAX3)).toBeFalsy();
}); });
}); });
@ -51,7 +53,7 @@ const deps = { ...dependencies, ...devDependencies };
// Skip the test if win-ca is not installed, or os is not win32 // Skip the test if win-ca is not installed, or os is not win32
(deps["win-ca"] && os.platform().includes("win32") ? describe: describe.skip)("inject CA for Windows", () => { (deps["win-ca"] && os.platform().includes("win32") ? describe: describe.skip)("inject CA for Windows", () => {
// for reset https.globalAgent.options.ca after testing // for reset https.globalAgent.options.ca after testing
let _ca: string | Buffer | (string | Buffer)[]; let _ca: string | Buffer | (string | Buffer)[] | undefined;
beforeEach(() => { beforeEach(() => {
_ca = https.globalAgent.options.ca; _ca = https.globalAgent.options.ca;

View File

@ -30,7 +30,7 @@ import userStoreInjectable from "../user-store/user-store.injectable";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import type { ClusterStoreModel } from "../cluster-store/cluster-store"; import type { ClusterStoreModel } from "../cluster-store/cluster-store";
import { defaultTheme } from "../vars"; import { defaultThemeId } from "../vars";
import writeFileInjectable from "../fs/write-file.injectable"; import writeFileInjectable from "../fs/write-file.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable import getConfigurationFileModelInjectable
@ -49,7 +49,7 @@ describe("user store tests", () => {
mockFs(); mockFs();
di.override(writeFileInjectable, () => () => undefined); di.override(writeFileInjectable, () => () => Promise.resolve());
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(userStoreInjectable, () => UserStore.createInstance()); di.override(userStoreInjectable, () => UserStore.createInstance());
@ -80,7 +80,7 @@ describe("user store tests", () => {
userStore.httpsProxy = "abcd://defg"; userStore.httpsProxy = "abcd://defg";
expect(userStore.httpsProxy).toBe("abcd://defg"); expect(userStore.httpsProxy).toBe("abcd://defg");
expect(userStore.colorTheme).toBe(defaultTheme); expect(userStore.colorTheme).toBe(defaultThemeId);
userStore.colorTheme = "light"; userStore.colorTheme = "light";
expect(userStore.colorTheme).toBe("light"); expect(userStore.colorTheme).toBe("light");
@ -89,7 +89,7 @@ describe("user store tests", () => {
it("correctly resets theme to default value", async () => { it("correctly resets theme to default value", async () => {
userStore.colorTheme = "some other theme"; userStore.colorTheme = "some other theme";
userStore.resetTheme(); userStore.resetTheme();
expect(userStore.colorTheme).toBe(defaultTheme); expect(userStore.colorTheme).toBe(defaultThemeId);
}); });
it("correctly calculates if the last seen version is an old release", () => { it("correctly calculates if the last seen version is an old release", () => {

View File

@ -2,20 +2,30 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { kubernetesClusterCategory } from "../kubernetes-cluster";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
import kubernetesClusterCategoryInjectable from "../../catalog/categories/kubernetes-cluster.injectable";
import type { KubernetesClusterCategory } from "../kubernetes-cluster";
describe("kubernetesClusterCategory", () => { describe("kubernetesClusterCategory", () => {
let kubernetesClusterCategory: KubernetesClusterCategory;
beforeEach(() => {
const di = getDiForUnitTesting();
kubernetesClusterCategory = di.inject(kubernetesClusterCategoryInjectable);
});
describe("filteredItems", () => { describe("filteredItems", () => {
const item1 = { const item1 = {
icon: "Icon", icon: "Icon",
title: "Title", title: "Title",
// eslint-disable-next-line @typescript-eslint/no-empty-function
onClick: () => {}, onClick: () => {},
}; };
const item2 = { const item2 = {
icon: "Icon 2", icon: "Icon 2",
title: "Title 2", title: "Title 2",
// eslint-disable-next-line @typescript-eslint/no-empty-function
onClick: () => {}, onClick: () => {},
}; };

View File

@ -5,8 +5,7 @@
import { navigate } from "../../renderer/navigation"; import { navigate } from "../../renderer/navigation";
import type { CatalogEntityMetadata, CatalogEntitySpec, CatalogEntityStatus } from "../catalog"; import type { CatalogEntityMetadata, CatalogEntitySpec, CatalogEntityStatus } from "../catalog";
import { CatalogCategory, CatalogEntity } from "../catalog"; import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity";
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
interface GeneralEntitySpec extends CatalogEntitySpec { interface GeneralEntitySpec extends CatalogEntitySpec {
path: string; path: string;
@ -23,18 +22,6 @@ export class GeneralEntity extends CatalogEntity<CatalogEntityMetadata, CatalogE
async onRun() { async onRun() {
navigate(this.spec.path); navigate(this.spec.path);
} }
public onSettingsOpen(): void {
return;
}
public onDetailsOpen(): void {
return;
}
public onContextMenuOpen(): void {
return;
}
} }
export class GeneralCategory extends CatalogCategory { export class GeneralCategory extends CatalogCategory {
@ -47,15 +34,10 @@ export class GeneralCategory extends CatalogCategory {
public spec = { public spec = {
group: "entity.k8slens.dev", group: "entity.k8slens.dev",
versions: [ versions: [
{ categoryVersion("v1alpha1", GeneralEntity),
name: "v1alpha1",
entityClass: GeneralEntity,
},
], ],
names: { names: {
kind: "General", kind: "General",
}, },
}; };
} }
catalogCategoryRegistry.add(new GeneralCategory());

View File

@ -3,13 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import type { CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategorySpec } from "../catalog"; import type { CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategorySpec } from "../catalog";
import { CatalogEntity, CatalogCategory } from "../catalog"; import { CatalogEntity, CatalogCategory, categoryVersion } from "../catalog/catalog-entity";
import { ClusterStore } from "../cluster-store/cluster-store"; import { ClusterStore } from "../cluster-store/cluster-store";
import { broadcastMessage } from "../ipc"; import { broadcastMessage } from "../ipc";
import { app } from "electron"; import { app } from "electron";
import type { CatalogEntitySpec } from "../catalog/catalog-entity"; import type { CatalogEntityConstructor, CatalogEntitySpec } from "../catalog/catalog-entity";
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc"; import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc";
import KubeClusterCategoryIcon from "./icons/kubernetes.svg"; import KubeClusterCategoryIcon from "./icons/kubernetes.svg";
@ -60,6 +59,10 @@ export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconn
export interface KubernetesClusterStatus extends CatalogEntityStatus { export interface KubernetesClusterStatus extends CatalogEntityStatus {
} }
export function isKubernetesCluster(item: unknown): item is KubernetesCluster {
return item instanceof KubernetesCluster;
}
export class KubernetesCluster< export class KubernetesCluster<
Metadata extends KubernetesClusterMetadata = KubernetesClusterMetadata, Metadata extends KubernetesClusterMetadata = KubernetesClusterMetadata,
Status extends KubernetesClusterStatus = KubernetesClusterStatus, Status extends KubernetesClusterStatus = KubernetesClusterStatus,
@ -99,7 +102,7 @@ export class KubernetesCluster<
// //
} }
async onContextMenuOpen(context: CatalogEntityContextMenuContext) { onContextMenuOpen(context: CatalogEntityContextMenuContext) {
if (!this.metadata.source || this.metadata.source === "local") { if (!this.metadata.source || this.metadata.source === "local") {
context.menuItems.push({ context.menuItems.push({
title: "Settings", title: "Settings",
@ -128,14 +131,10 @@ export class KubernetesCluster<
}); });
break; break;
} }
catalogCategoryRegistry
.getCategoryForEntity<KubernetesClusterCategory>(this)
?.emit("contextMenuOpen", this, context);
} }
} }
class KubernetesClusterCategory extends CatalogCategory { export class KubernetesClusterCategory extends CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1"; public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
public readonly kind = "CatalogCategory"; public readonly kind = "CatalogCategory";
public metadata = { public metadata = {
@ -145,17 +144,10 @@ class KubernetesClusterCategory extends CatalogCategory {
public spec: CatalogCategorySpec = { public spec: CatalogCategorySpec = {
group: "entity.k8slens.dev", group: "entity.k8slens.dev",
versions: [ versions: [
{ categoryVersion("v1alpha1", KubernetesCluster as CatalogEntityConstructor<KubernetesCluster>),
name: "v1alpha1",
entityClass: KubernetesCluster,
},
], ],
names: { names: {
kind: "KubernetesCluster", kind: "KubernetesCluster",
}, },
}; };
} }
export const kubernetesClusterCategory = new KubernetesClusterCategory();
catalogCategoryRegistry.add(kubernetesClusterCategory);

View File

@ -4,8 +4,7 @@
*/ */
import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { CatalogCategory, CatalogEntity } from "../catalog"; import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity";
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { productName } from "../vars"; import { productName } from "../vars";
import { WeblinkStore } from "../weblink-store"; import { WeblinkStore } from "../weblink-store";
@ -30,11 +29,7 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
window.open(this.spec.url, "_blank"); window.open(this.spec.url, "_blank");
} }
public onSettingsOpen(): void { onContextMenuOpen(context: CatalogEntityContextMenuContext) {
return;
}
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
if (this.metadata.source === "local") { if (this.metadata.source === "local") {
context.menuItems.push({ context.menuItems.push({
title: "Delete", title: "Delete",
@ -45,10 +40,6 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
}, },
}); });
} }
catalogCategoryRegistry
.getCategoryForEntity<WebLinkCategory>(this)
?.emit("contextMenuOpen", this, context);
} }
} }
@ -62,15 +53,10 @@ export class WebLinkCategory extends CatalogCategory {
public spec = { public spec = {
group: "entity.k8slens.dev", group: "entity.k8slens.dev",
versions: [ versions: [
{ categoryVersion("v1alpha1", WebLink),
name: "v1alpha1",
entityClass: WebLink,
},
], ],
names: { names: {
kind: "WebLink", kind: "WebLink",
}, },
}; };
} }
catalogCategoryRegistry.add(new WebLinkCategory());

View File

@ -3,96 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { action, computed, observable, makeObservable } from "mobx"; import { asLegacyGlobalForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import { once } from "lodash"; import catalogCategoryRegistryInjectable from "./category-registry.injectable";
import { iter, getOrInsertMap, strictSet } from "../utils";
import type { Disposer } from "../utils";
import type { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
export type CategoryFilter = (category: CatalogCategory) => any; /**
* @deprecated use `di.inject(catalogCategoryRegistryInjectable)` instead
export class CatalogCategoryRegistry { */
protected categories = observable.set<CatalogCategory>(); export const catalogCategoryRegistry = asLegacyGlobalForExtensionApi(catalogCategoryRegistryInjectable);
protected groupKinds = new Map<string, Map<string, CatalogCategory>>();
protected filters = observable.set<CategoryFilter>([], {
deep: false,
});
constructor() {
makeObservable(this);
}
@action add(category: CatalogCategory): Disposer {
const byGroup = getOrInsertMap(this.groupKinds, category.spec.group);
this.categories.add(category);
strictSet(byGroup, category.spec.names.kind, category);
return () => {
this.categories.delete(category);
byGroup.delete(category.spec.names.kind);
};
}
@computed get items() {
return Array.from(this.categories);
}
@computed get filteredItems() {
return Array.from(
iter.reduce(
this.filters,
iter.filter,
this.items.values(),
),
);
}
getForGroupKind<T extends CatalogCategory>(group: string, kind: string): T | undefined {
return this.groupKinds.get(group)?.get(kind) as T;
}
getEntityForData(data: CatalogEntityData & CatalogEntityKindData) {
const category = this.getCategoryForEntity(data);
if (!category) {
return null;
}
const splitApiVersion = data.apiVersion.split("/");
const version = splitApiVersion[1];
const specVersion = category.spec.versions.find((v) => v.name === version);
if (!specVersion) {
return null;
}
return new specVersion.entityClass(data);
}
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData & CatalogEntityKindData): T | undefined {
const splitApiVersion = data.apiVersion.split("/");
const group = splitApiVersion[0];
return this.getForGroupKind(group, data.kind);
}
getByName(name: string) {
return this.items.find(category => category.metadata?.name == name);
}
/**
* Add a new filter to the set of category filters
* @param fn The function that should return a truthy value if that category should be displayed
* @returns A function to remove that filter
*/
addCatalogCategoryFilter(fn: CategoryFilter): Disposer {
this.filters.add(fn);
return once(() => void this.filters.delete(fn));
}
}
export const catalogCategoryRegistry = new CatalogCategoryRegistry();

View File

@ -11,19 +11,19 @@ import type { Disposer } from "../utils";
import { iter } from "../utils"; import { iter } from "../utils";
import type { CategoryColumnRegistration } from "../../renderer/components/+catalog/custom-category-columns"; import type { CategoryColumnRegistration } from "../../renderer/components/+catalog/custom-category-columns";
type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never; export type CatalogEntityDataFor<Entity> = Entity extends CatalogEntity<infer Metadata, infer Status, infer Spec>
type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never; ? CatalogEntityData<Metadata, Status, Spec>
type ExtractEntitySpecType<Entity> = Entity extends CatalogEntity<any, any, infer Spec> ? Spec : never; : never;
export type CatalogEntityInstanceFrom<Constructor> = Constructor extends CatalogEntityConstructor<infer Entity>
? Entity
: never;
export type CatalogEntityConstructor<Entity extends CatalogEntity> = ( export type CatalogEntityConstructor<Entity extends CatalogEntity> = (
(new (data: CatalogEntityData< new (data: CatalogEntityDataFor<Entity>) => Entity
ExtractEntityMetadataType<Entity>,
ExtractEntityStatusType<Entity>,
ExtractEntitySpecType<Entity>
>) => Entity)
); );
export interface CatalogCategoryVersion<Entity extends CatalogEntity> { export interface CatalogCategoryVersion {
/** /**
* The specific version that the associated constructor is for. This MUST be * The specific version that the associated constructor is for. This MUST be
* a DNS label and SHOULD be of the form `vN`, `vNalphaY`, or `vNbetaY` where * a DNS label and SHOULD be of the form `vN`, `vNalphaY`, or `vNbetaY` where
@ -35,19 +35,19 @@ export interface CatalogCategoryVersion<Entity extends CatalogEntity> {
* - `v1alpha2` * - `v1alpha2`
* - `v3beta2` * - `v3beta2`
*/ */
name: string; readonly name: string;
/** /**
* The constructor for the entities. * The constructor for the entities.
*/ */
entityClass: CatalogEntityConstructor<Entity>; readonly entityClass: CatalogEntityConstructor<CatalogEntity>;
} }
export interface CatalogCategorySpec { export interface CatalogCategorySpec {
/** /**
* The grouping for for the category. This MUST be a DNS label. * The grouping for for the category. This MUST be a DNS label.
*/ */
group: string; readonly group: string;
/** /**
* The specific versions of the constructors. * The specific versions of the constructors.
@ -56,18 +56,18 @@ export interface CatalogCategorySpec {
* For example, if `group = "entity.k8slens.dev"` and there is an entry in `.versions` with * For example, if `group = "entity.k8slens.dev"` and there is an entry in `.versions` with
* `name = "v1alpha1"` then the resulting `.apiVersion` MUST be `entity.k8slens.dev/v1alpha1` * `name = "v1alpha1"` then the resulting `.apiVersion` MUST be `entity.k8slens.dev/v1alpha1`
*/ */
versions: CatalogCategoryVersion<CatalogEntity>[]; readonly versions: CatalogCategoryVersion[];
/** /**
* This is the concerning the category * This is the concerning the category
*/ */
names: { readonly names: {
/** /**
* The kind of entity that this category is for. This value MUST be a DNS * The kind of entity that this category is for. This value MUST be a DNS
* label and MUST be equal to the `kind` fields that are produced by the * label and MUST be equal to the `kind` fields that are produced by the
* `.versions.[] | .entityClass` fields. * `.versions.[] | .entityClass` fields.
*/ */
kind: string; readonly kind: string;
}; };
/** /**
@ -81,7 +81,7 @@ export interface CatalogCategorySpec {
* *
* These columns will not be used in the "Browse" view. * These columns will not be used in the "Browse" view.
*/ */
displayColumns?: CategoryColumnRegistration[]; readonly displayColumns?: CategoryColumnRegistration[];
} }
/** /**
@ -109,6 +109,30 @@ export interface CatalogCategoryEvents {
contextMenuOpen: (entity: CatalogEntity, context: CatalogEntityContextMenuContext) => void; contextMenuOpen: (entity: CatalogEntity, context: CatalogEntityContextMenuContext) => void;
} }
export interface CatalogCategoryMetadata {
/**
* The name of your category. The category can be searched for by this
* value. This will also be used for the catalog menu.
*/
readonly name: string;
/**
* Either an `<svg>` or the name of an icon from {@link IconProps}
*/
readonly icon: string;
}
export function categoryVersion<
T extends CatalogEntity<Metadata, Status, Spec>,
Metadata extends CatalogEntityMetadata,
Status extends CatalogEntityStatus,
Spec extends CatalogEntitySpec,
>(name: string, entityClass: new (data: CatalogEntityData<Metadata, Status, Spec>) => T): CatalogCategoryVersion {
return {
name,
entityClass: entityClass as CatalogEntityConstructor<T>,
};
}
export abstract class CatalogCategory extends (EventEmitter as new () => TypedEmitter<CatalogCategoryEvents>) { export abstract class CatalogCategory extends (EventEmitter as new () => TypedEmitter<CatalogCategoryEvents>) {
/** /**
* The version of category that you are wanting to declare. * The version of category that you are wanting to declare.
@ -131,28 +155,17 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
/** /**
* The data about the category itself * The data about the category itself
*/ */
abstract readonly metadata: { abstract readonly metadata: CatalogCategoryMetadata;
/**
* The name of your category. The category can be searched for by this
* value. This will also be used for the catalog menu.
*/
name: string;
/**
* Either an `<svg>` or the name of an icon from {@link IconProps}
*/
icon: string;
};
/** /**
* The most important part of a category, as it is where entity versions are declared. * The most important part of a category, as it is where entity versions are declared.
*/ */
abstract spec: CatalogCategorySpec; abstract readonly spec: CatalogCategorySpec;
/** /**
* @internal * @internal
*/ */
protected filters = observable.set<AddMenuFilter>([], { protected readonly filters = observable.set<AddMenuFilter>([], {
deep: false, deep: false,
}); });
@ -217,14 +230,16 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
} }
} }
export interface CatalogEntityMetadata { export type EntityMetadataObject = { [Key in string]?: EntityMetadataValue };
export type EntityMetadataValue = string | number | boolean | EntityMetadataObject | undefined;
export interface CatalogEntityMetadata extends EntityMetadataObject {
uid: string; uid: string;
name: string; name: string;
shortName?: string; shortName?: string;
description?: string; description?: string;
source?: string; source?: string;
labels: Record<string, string>; labels: Record<string, string>;
[key: string]: string | object;
} }
export interface CatalogEntityStatus { export interface CatalogEntityStatus {
@ -392,7 +407,7 @@ export abstract class CatalogEntity<
return this.status.enabled ?? true; return this.status.enabled ?? true;
} }
public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>; public onRun?(context: CatalogEntityActionContext): void | Promise<void>;
public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>; public onContextMenuOpen?(context: CatalogEntityContextMenuContext): void | Promise<void>;
public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>; public onSettingsOpen?(context: CatalogEntitySettingsContext): void | Promise<void>;
} }

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { GeneralCategory } from "../../catalog-entities";
import { builtInCategoryInjectionToken } from "../category-registry.injectable";
const generalCategoryInjectable = getInjectable({
id: "general-category",
instantiate: () => new GeneralCategory(),
injectionToken: builtInCategoryInjectionToken,
});
export default generalCategoryInjectable;

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { KubernetesClusterCategory } from "../../catalog-entities/kubernetes-cluster";
import { builtInCategoryInjectionToken } from "../category-registry.injectable";
const kubernetesClusterCategoryInjectable = getInjectable({
id: "kubernetes-cluster-category",
instantiate: () => new KubernetesClusterCategory(),
injectionToken: builtInCategoryInjectionToken,
});
export default kubernetesClusterCategoryInjectable;

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { WebLinkCategory } from "../../catalog-entities";
import { builtInCategoryInjectionToken } from "../category-registry.injectable";
const weblinkCategoryInjectable = getInjectable({
id: "weblink-category",
instantiate: () => new WebLinkCategory(),
injectionToken: builtInCategoryInjectionToken,
});
export default weblinkCategoryInjectable;

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import type { CatalogCategory } from "./catalog-entity";
import { CatalogCategoryRegistry } from "./category-registry";
export const builtInCategoryInjectionToken = getInjectionToken<CatalogCategory>({
id: "built-in-category-token",
});
const catalogCategoryRegistryInjectable = getInjectable({
id: "catalog-category-registry",
instantiate: (di) => {
const registry = new CatalogCategoryRegistry();
const categories = di.injectMany(builtInCategoryInjectionToken);
for (const category of categories) {
registry.add(category);
}
return registry;
},
});
export default catalogCategoryRegistryInjectable;

View File

@ -0,0 +1,103 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, computed, observable, makeObservable } from "mobx";
import { once } from "lodash";
import { iter, getOrInsertMap, strictSet } from "../utils";
import type { Disposer } from "../utils";
import type { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
export type CategoryFilter = (category: CatalogCategory) => any;
export class CatalogCategoryRegistry {
protected readonly categories = observable.set<CatalogCategory>();
protected readonly groupKinds = new Map<string, Map<string, CatalogCategory>>();
protected readonly filters = observable.set<CategoryFilter>([], {
deep: false,
});
constructor() {
makeObservable(this);
}
@action add(category: CatalogCategory): Disposer {
const byGroup = getOrInsertMap(this.groupKinds, category.spec.group);
this.categories.add(category);
strictSet(byGroup, category.spec.names.kind, category);
return () => {
this.categories.delete(category);
byGroup.delete(category.spec.names.kind);
};
}
@computed get items() {
return Array.from(this.categories);
}
@computed get filteredItems() {
return Array.from(
iter.reduce(
this.filters,
iter.filter,
this.items.values(),
),
);
}
getForGroupKind<T extends CatalogCategory>(group: string, kind: string): T | undefined {
return this.groupKinds.get(group)?.get(kind) as T;
}
getEntityForData(data: CatalogEntityData & CatalogEntityKindData) {
const category = this.getCategoryForEntity(data);
if (!category) {
return null;
}
const splitApiVersion = data.apiVersion.split("/");
const version = splitApiVersion[1];
const specVersion = category.spec.versions.find((v) => v.name === version);
if (!specVersion) {
return null;
}
return new specVersion.entityClass(data);
}
hasCategoryForEntity({ kind, apiVersion }: CatalogEntityData & CatalogEntityKindData): boolean {
const splitApiVersion = apiVersion.split("/");
const group = splitApiVersion[0];
return this.groupKinds.get(group)?.has(kind) ?? false;
}
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData & CatalogEntityKindData): T | undefined {
const splitApiVersion = data.apiVersion.split("/");
const group = splitApiVersion[0];
return this.getForGroupKind(group, data.kind);
}
getByName(name: string) {
return this.items.find(category => category.metadata?.name == name);
}
/**
* Add a new filter to the set of category filters
* @param fn The function that should return a truthy value if that category should be displayed
* @returns A function to remove that filter
*/
addCatalogCategoryFilter(fn: CategoryFilter): Disposer {
this.filters.add(fn);
return once(() => void this.filters.delete(fn));
}
}

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
import catalogCategoryRegistryInjectable from "./category-registry.injectable";
export type HasCategoryForEntity = (data: CatalogEntityData & CatalogEntityKindData) => boolean;
const hasCategoryForEntityInjectable = getInjectable({
id: "has-category-for-entity",
instantiate: (di): HasCategoryForEntity => {
const registry = di.inject(catalogCategoryRegistryInjectable);
return (data) => registry.hasCategoryForEntity(data);
},
});
export default hasCategoryForEntityInjectable;

View File

@ -4,4 +4,5 @@
*/ */
export * from "./catalog-category-registry"; export * from "./catalog-category-registry";
export * from "./category-registry";
export * from "./catalog-entity"; export * from "./catalog-entity";

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { CatalogEntity, CatalogEntityContextMenuContext } from "./catalog-entity";
import catalogCategoryRegistryInjectable from "./category-registry.injectable";
export type VisitEntityContextMenu = (entity: CatalogEntity, context: CatalogEntityContextMenuContext) => void;
const visitEntityContextMenuInjectable = getInjectable({
id: "visit-entity-context-menu",
instantiate: (di): VisitEntityContextMenu => {
const categoryRegistry = di.inject(catalogCategoryRegistryInjectable);
return (entity, context) => {
entity.onContextMenuOpen?.(context);
categoryRegistry.getCategoryForEntity(entity)?.emit("contextMenuOpen", entity, context);
};
},
});
export default visitEntityContextMenuInjectable;

View File

@ -12,7 +12,7 @@ const allowedResourcesInjectable = getInjectable({
instantiate: (di) => { instantiate: (di) => {
const cluster = di.inject(hostedClusterInjectable); const cluster = di.inject(hostedClusterInjectable);
return computed(() => new Set(cluster.allowedResources), { return computed(() => new Set(cluster?.allowedResources), {
// This needs to be here so that during refresh changes are only propogated when necessary // This needs to be here so that during refresh changes are only propogated when necessary
equals: (cur, prev) => comparer.structural(cur, prev), equals: (cur, prev) => comparer.structural(cur, prev),
}); });

View File

@ -103,8 +103,12 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return this.clusters.size > 0; return this.clusters.size > 0;
} }
getById(id: ClusterId): Cluster | null { getById(id: ClusterId | undefined): Cluster | undefined {
return this.clusters.get(id) ?? null; if (id) {
return this.clusters.get(id);
}
return undefined;
} }
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster { addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {

View File

@ -3,13 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { daemonSetStore } from "./daemonsets.store"; import { getClusterIdFromHost } from "../utils";
const daemonsetsStoreInjectable = getInjectable({ const hostedClusterIdInjectable = getInjectable({
id: "daemonsets-store", id: "hosted-cluster-id",
instantiate: () => daemonSetStore, instantiate: () => getClusterIdFromHost(location.host),
causesSideEffects: true, causesSideEffects: true,
}); });
export default daemonsetsStoreInjectable; export default hostedClusterIdInjectable;

View File

@ -3,16 +3,17 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { getHostedClusterId } from "../utils"; import hostedClusterIdInjectable from "./hosted-cluster-id.injectable";
import clusterStoreInjectable from "./cluster-store.injectable"; import clusterStoreInjectable from "./cluster-store.injectable";
const hostedClusterInjectable = getInjectable({ const hostedClusterInjectable = getInjectable({
id: "hosted-cluster", id: "hosted-cluster",
instantiate: (di) => { instantiate: (di) => {
const hostedClusterId = getHostedClusterId(); const hostedClusterId = di.inject(hostedClusterIdInjectable);
const store = di.inject(clusterStoreInjectable);
return di.inject(clusterStoreInjectable).getById(hostedClusterId); return store.getById(hostedClusterId);
}, },
}); });

View File

@ -3,10 +3,9 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { ipcMain } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx"; import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
import { broadcastMessage } from "../ipc"; import { broadcastMessage } from "../ipc";
import type { ContextHandler } from "../../main/context-handler/context-handler"; import type { ClusterContextHandler } from "../../main/context-handler/context-handler";
import type { KubeConfig } from "@kubernetes/client-node"; import type { KubeConfig } from "@kubernetes/client-node";
import { HttpError } from "@kubernetes/client-node"; import { HttpError } from "@kubernetes/client-node";
import type { Kubectl } from "../../main/kubectl/kubectl"; import type { Kubectl } from "../../main/kubectl/kubectl";
@ -14,22 +13,24 @@ import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig
import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "../kube-helpers"; import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "../kube-helpers";
import type { KubeApiResource, KubeResource } from "../rbac"; import type { KubeApiResource, KubeResource } from "../rbac";
import { apiResourceRecord, apiResources } from "../rbac"; import { apiResourceRecord, apiResources } from "../rbac";
import logger from "../../main/logger";
import { VersionDetector } from "../../main/cluster-detectors/version-detector"; import { VersionDetector } from "../../main/cluster-detectors/version-detector";
import { DetectorRegistry } from "../../main/cluster-detectors/detector-registry"; import { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
import plimit from "p-limit"; import plimit from "p-limit";
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types"; import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types"; import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
import { disposer, toJS } from "../utils"; import { disposer, isDefined, isRequestError, toJS } from "../utils";
import type { Response } from "request"; import type { Response } from "request";
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster"; import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
import type { CanI } from "./authorization-review.injectable"; import type { CanI } from "./authorization-review.injectable";
import type { ListNamespaces } from "./list-namespaces.injectable"; import type { ListNamespaces } from "./list-namespaces.injectable";
import assert from "assert";
import type { Logger } from "../logger";
export interface ClusterDependencies { export interface ClusterDependencies {
readonly directoryForKubeConfigs: string; readonly directoryForKubeConfigs: string;
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; readonly logger: Logger;
createContextHandler: (cluster: Cluster) => ContextHandler; createKubeconfigManager: (cluster: Cluster) => KubeconfigManager | undefined;
createContextHandler: (cluster: Cluster) => ClusterContextHandler | undefined;
createKubectl: (clusterVersion: string) => Kubectl; createKubectl: (clusterVersion: string) => Kubectl;
createAuthorizationReview: (config: KubeConfig) => CanI; createAuthorizationReview: (config: KubeConfig) => CanI;
createListNamespaces: (config: KubeConfig) => ListNamespaces; createListNamespaces: (config: KubeConfig) => ListNamespaces;
@ -43,17 +44,31 @@ export interface ClusterDependencies {
export class Cluster implements ClusterModel, ClusterState { export class Cluster implements ClusterModel, ClusterState {
/** Unique id for a cluster */ /** Unique id for a cluster */
public readonly id: ClusterId; public readonly id: ClusterId;
private kubeCtl: Kubectl; private kubeCtl: Kubectl | undefined;
/** /**
* Context handler * Context handler
* *
* @internal * @internal
*/ */
public contextHandler: ContextHandler; protected readonly _contextHandler: ClusterContextHandler | undefined;
protected proxyKubeconfigManager: KubeconfigManager; protected readonly _proxyKubeconfigManager: KubeconfigManager | undefined;
protected eventsDisposer = disposer(); protected readonly eventsDisposer = disposer();
protected activated = false; protected activated = false;
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map(); private readonly resourceAccessStatuses = new Map<KubeApiResource, boolean>();
public get contextHandler() {
// TODO: remove these once main/renderer are seperate classes
assert(this._contextHandler, "contextHandler is only defined in the main environment");
return this._contextHandler;
}
protected get proxyKubeconfigManager() {
// TODO: remove these once main/renderer are seperate classes
assert(this._proxyKubeconfigManager, "proxyKubeconfigManager is only defined in the main environment");
return this._proxyKubeconfigManager;
}
get whenReady() { get whenReady() {
return when(() => this.ready); return when(() => this.ready);
@ -64,21 +79,21 @@ export class Cluster implements ClusterModel, ClusterState {
* *
* @observable * @observable
*/ */
@observable contextName: string; @observable contextName!: string;
/** /**
* Path to kubeconfig * Path to kubeconfig
* *
* @observable * @observable
*/ */
@observable kubeConfigPath: string; @observable kubeConfigPath!: string;
/** /**
* @deprecated * @deprecated
*/ */
@observable workspace: string; @observable workspace?: string;
/** /**
* @deprecated * @deprecated
*/ */
@observable workspaces: string[]; @observable workspaces?: string[];
/** /**
* Kubernetes API server URL * Kubernetes API server URL
* *
@ -215,7 +230,7 @@ export class Cluster implements ClusterModel, ClusterState {
* @computed * @computed
* @internal * @internal
*/ */
@computed get defaultNamespace(): string { @computed get defaultNamespace(): string | undefined {
return this.preferences.defaultNamespace; return this.preferences.defaultNamespace;
} }
@ -231,19 +246,24 @@ export class Cluster implements ClusterModel, ClusterState {
throw validationError; throw validationError;
} }
this.apiUrl = config.getCluster(config.getContextObject(this.contextName).cluster).server; const context = config.getContextObject(this.contextName);
if (ipcMain) { assert(context);
// for the time being, until renderer gets its own cluster type
this.contextHandler = this.dependencies.createContextHandler(this);
this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
logger.debug(`[CLUSTER]: Cluster init success`, { const cluster = config.getCluster(context.cluster);
id: this.id,
context: this.contextName, assert(cluster);
apiUrl: this.apiUrl,
}); this.apiUrl = cluster.server;
}
// for the time being, until renderer gets its own cluster type
this._contextHandler = this.dependencies.createContextHandler(this);
this._proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
this.dependencies.logger.debug(`[CLUSTER]: Cluster init success`, {
id: this.id,
context: this.contextName,
apiUrl: this.apiUrl,
});
} }
/** /**
@ -255,6 +275,7 @@ export class Cluster implements ClusterModel, ClusterState {
// Note: do not assign ID as that should never be updated // Note: do not assign ID as that should never be updated
this.kubeConfigPath = model.kubeConfigPath; this.kubeConfigPath = model.kubeConfigPath;
this.contextName = model.contextName;
if (model.workspace) { if (model.workspace) {
this.workspace = model.workspace; this.workspace = model.workspace;
@ -264,10 +285,6 @@ export class Cluster implements ClusterModel, ClusterState {
this.workspaces = model.workspaces; this.workspaces = model.workspaces;
} }
if (model.contextName) {
this.contextName = model.contextName;
}
if (model.preferences) { if (model.preferences) {
this.preferences = model.preferences; this.preferences = model.preferences;
} }
@ -289,7 +306,7 @@ export class Cluster implements ClusterModel, ClusterState {
* @internal * @internal
*/ */
protected bindEvents() { protected bindEvents() {
logger.info(`[CLUSTER]: bind events`, this.getMeta()); this.dependencies.logger.info(`[CLUSTER]: bind events`, this.getMeta());
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes
@ -310,13 +327,13 @@ export class Cluster implements ClusterModel, ClusterState {
* @internal * @internal
*/ */
protected async recreateProxyKubeconfig() { protected async recreateProxyKubeconfig() {
logger.info("[CLUSTER]: Recreating proxy kubeconfig"); this.dependencies.logger.info("[CLUSTER]: Recreating proxy kubeconfig");
try { try {
await this.proxyKubeconfigManager.clear(); await this.proxyKubeconfigManager.clear();
await this.getProxyKubeconfig(); await this.getProxyKubeconfig();
} catch (error) { } catch (error) {
logger.error(`[CLUSTER]: failed to recreate proxy kubeconfig`, error); this.dependencies.logger.error(`[CLUSTER]: failed to recreate proxy kubeconfig`, error);
} }
} }
@ -330,7 +347,7 @@ export class Cluster implements ClusterModel, ClusterState {
return this.pushState(); return this.pushState();
} }
logger.info(`[CLUSTER]: activate`, this.getMeta()); this.dependencies.logger.info(`[CLUSTER]: activate`, this.getMeta());
if (!this.eventsDisposer.length) { if (!this.eventsDisposer.length) {
this.bindEvents(); this.bindEvents();
@ -348,7 +365,7 @@ export class Cluster implements ClusterModel, ClusterState {
await this.refreshAccessibility(); await this.refreshAccessibility();
// download kubectl in background, so it's not blocking dashboard // download kubectl in background, so it's not blocking dashboard
this.ensureKubectl() this.ensureKubectl()
.catch(error => logger.warn(`[CLUSTER]: failed to download kubectl for clusterId=${this.id}`, error)); .catch(error => this.dependencies.logger.warn(`[CLUSTER]: failed to download kubectl for clusterId=${this.id}`, error));
this.broadcastConnectUpdate("Connected, waiting for view to load ..."); this.broadcastConnectUpdate("Connected, waiting for view to load ...");
} }
@ -372,9 +389,8 @@ export class Cluster implements ClusterModel, ClusterState {
*/ */
@action @action
async reconnect() { async reconnect() {
logger.info(`[CLUSTER]: reconnect`, this.getMeta()); this.dependencies.logger.info(`[CLUSTER]: reconnect`, this.getMeta());
this.contextHandler?.stopServer(); await this.contextHandler?.restartServer();
await this.contextHandler?.ensureServer();
this.disconnected = false; this.disconnected = false;
} }
@ -383,10 +399,10 @@ export class Cluster implements ClusterModel, ClusterState {
*/ */
@action disconnect(): void { @action disconnect(): void {
if (this.disconnected) { if (this.disconnected) {
return void logger.debug("[CLUSTER]: already disconnected", { id: this.id }); return void this.dependencies.logger.debug("[CLUSTER]: already disconnected", { id: this.id });
} }
logger.info(`[CLUSTER]: disconnecting`, { id: this.id }); this.dependencies.logger.info(`[CLUSTER]: disconnecting`, { id: this.id });
this.eventsDisposer(); this.eventsDisposer();
this.contextHandler?.stopServer(); this.contextHandler?.stopServer();
this.disconnected = true; this.disconnected = true;
@ -397,7 +413,7 @@ export class Cluster implements ClusterModel, ClusterState {
this.allowedNamespaces = []; this.allowedNamespaces = [];
this.resourceAccessStatuses.clear(); this.resourceAccessStatuses.clear();
this.pushState(); this.pushState();
logger.info(`[CLUSTER]: disconnected`, { id: this.id }); this.dependencies.logger.info(`[CLUSTER]: disconnected`, { id: this.id });
} }
/** /**
@ -406,7 +422,7 @@ export class Cluster implements ClusterModel, ClusterState {
*/ */
@action @action
async refresh(opts: ClusterRefreshOptions = {}) { async refresh(opts: ClusterRefreshOptions = {}) {
logger.info(`[CLUSTER]: refresh`, this.getMeta()); this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta());
await this.refreshConnectionStatus(); await this.refreshConnectionStatus();
if (this.accessible) { if (this.accessible) {
@ -424,7 +440,7 @@ export class Cluster implements ClusterModel, ClusterState {
*/ */
@action @action
async refreshMetadata() { async refreshMetadata() {
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta()); this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
const metadata = await DetectorRegistry.getInstance().detectForCluster(this); const metadata = await DetectorRegistry.getInstance().detectForCluster(this);
const existingMetadata = this.metadata; const existingMetadata = this.metadata;
@ -495,11 +511,31 @@ export class Cluster implements ClusterModel, ClusterState {
return ClusterStatus.AccessGranted; return ClusterStatus.AccessGranted;
} catch (error) { } catch (error) {
logger.error(`[CLUSTER]: Failed to connect to "${this.contextName}": ${error}`); this.dependencies.logger.error(`[CLUSTER]: Failed to connect to "${this.contextName}": ${error}`);
if (error.statusCode) { if (isRequestError(error)) {
if (error.statusCode >= 400 && error.statusCode < 500) { if (error.statusCode) {
this.broadcastConnectUpdate("Invalid credentials", true); if (error.statusCode >= 400 && error.statusCode < 500) {
this.broadcastConnectUpdate("Invalid credentials", true);
return ClusterStatus.AccessDenied;
}
const message = String(error.error || error.message) || String(error);
this.broadcastConnectUpdate(message, true);
return ClusterStatus.Offline;
}
if (error.failed === true) {
if (error.timedOut === true) {
this.broadcastConnectUpdate("Connection timed out", true);
return ClusterStatus.Offline;
}
this.broadcastConnectUpdate("Failed to fetch credentials", true);
return ClusterStatus.AccessDenied; return ClusterStatus.AccessDenied;
} }
@ -507,26 +543,10 @@ export class Cluster implements ClusterModel, ClusterState {
const message = String(error.error || error.message) || String(error); const message = String(error.error || error.message) || String(error);
this.broadcastConnectUpdate(message, true); this.broadcastConnectUpdate(message, true);
} else {
return ClusterStatus.Offline; this.broadcastConnectUpdate("Unknown error has occurred", true);
} }
if (error.failed === true) {
if (error.timedOut === true) {
this.broadcastConnectUpdate("Connection timed out", true);
return ClusterStatus.Offline;
}
this.broadcastConnectUpdate("Failed to fetch credentials", true);
return ClusterStatus.AccessDenied;
}
const message = String(error.error || error.message) || String(error);
this.broadcastConnectUpdate(message, true);
return ClusterStatus.Offline; return ClusterStatus.Offline;
} }
} }
@ -575,7 +595,7 @@ export class Cluster implements ClusterModel, ClusterState {
* @param state cluster state * @param state cluster state
*/ */
pushState(state = this.getState()) { pushState(state = this.getState()) {
logger.silly(`[CLUSTER]: push-state`, state); this.dependencies.logger.silly(`[CLUSTER]: push-state`, state);
broadcastMessage("cluster:state", this.id, state); broadcastMessage("cluster:state", this.id, state);
} }
@ -598,7 +618,7 @@ export class Cluster implements ClusterModel, ClusterState {
broadcastConnectUpdate(message: string, isError = false): void { broadcastConnectUpdate(message: string, isError = false): void {
const update: KubeAuthUpdate = { message, isError }; const update: KubeAuthUpdate = { message, isError };
logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: this.getMeta() }); this.dependencies.logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: this.getMeta() });
broadcastMessage(`cluster:${this.id}:connection-update`, update); broadcastMessage(`cluster:${this.id}:connection-update`, update);
} }
@ -613,12 +633,12 @@ export class Cluster implements ClusterModel, ClusterState {
return await listNamespaces(); return await listNamespaces();
} catch (error) { } catch (error) {
const ctx = proxyConfig.getContextObject(this.contextName); const ctx = proxyConfig.getContextObject(this.contextName);
const namespaceList = [ctx.namespace].filter(Boolean); const namespaceList = [ctx?.namespace].filter(isDefined);
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) { if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
const { response } = error as HttpError & { response: Response }; const { response } = error as HttpError & { response: Response };
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body }); this.dependencies.logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body });
broadcastMessage(clusterListNamespaceForbiddenChannel, this.id); broadcastMessage(clusterListNamespaceForbiddenChannel, this.id);
} }

View File

@ -6,5 +6,8 @@ import { getInjectionToken } from "@ogre-tools/injectable";
import type { ClusterModel } from "../cluster-types"; import type { ClusterModel } from "../cluster-types";
import type { Cluster } from "./cluster"; import type { Cluster } from "./cluster";
export const createClusterInjectionToken = export type CreateCluster = (model: ClusterModel) => Cluster;
getInjectionToken<(model: ClusterModel) => Cluster>({ id: "create-cluster-token" });
export const createClusterInjectionToken = getInjectionToken<CreateCluster>({
id: "create-cluster-token",
});

View File

@ -5,6 +5,7 @@
import type { KubeConfig } from "@kubernetes/client-node"; import type { KubeConfig } from "@kubernetes/client-node";
import { CoreV1Api } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { isDefined } from "../utils";
export type ListNamespaces = () => Promise<string[]>; export type ListNamespaces = () => Promise<string[]>;
@ -14,7 +15,9 @@ export function listNamespaces(config: KubeConfig): ListNamespaces {
return async () => { return async () => {
const { body: { items }} = await coreApi.listNamespace(); const { body: { items }} = await coreApi.listNamespace();
return items.map(ns => ns.metadata.name); return items
.map(ns => ns.metadata?.name)
.filter(isDefined);
}; };
} }

View File

@ -14,12 +14,9 @@ describe("verify-that-all-routes-have-component", () => {
it("verify that routes have route component", async () => { it("verify that routes have route component", async () => {
const rendererDi = getDiForUnitTesting({ doGeneralOverrides: true }); const rendererDi = getDiForUnitTesting({ doGeneralOverrides: true });
rendererDi.override( rendererDi.override(clusterStoreInjectable, () => ({
clusterStoreInjectable, getById: () => null,
() => ({ getById: (): null => null } as unknown as ClusterStore), } as unknown as ClusterStore));
);
await rendererDi.runSetups();
const routes = rendererDi.injectMany(routeInjectionToken); const routes = rendererDi.injectMany(routeInjectionToken);
const routeComponents = rendererDi.injectMany( const routeComponents = rendererDi.injectMany(

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "./store.injectable";
import type { CreateHotbarData, CreateHotbarOptions } from "./types";
export type AddHotbar = (data: CreateHotbarData, opts?: CreateHotbarOptions) => void;
const addHotbarInjectable = getInjectable({
id: "add-hotbar",
instantiate: (di): AddHotbar => {
const store = di.inject(hotbarStoreInjectable);
return (data, opts) => store.add(data, opts);
},
});
export default addHotbarInjectable;

View File

@ -3,8 +3,9 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import catalogCatalogEntityInjectable from "./catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable"; import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import { HotbarStore } from "./hotbar-store"; import { HotbarStore } from "./store";
import loggerInjectable from "../logger.injectable";
const hotbarStoreInjectable = getInjectable({ const hotbarStoreInjectable = getInjectable({
id: "hotbar-store", id: "hotbar-store",
@ -14,6 +15,7 @@ const hotbarStoreInjectable = getInjectable({
return HotbarStore.createInstance({ return HotbarStore.createInstance({
catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable), catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable),
logger: di.inject(loggerInjectable),
}); });
}, },

View File

@ -4,22 +4,17 @@
*/ */
import { action, comparer, observable, makeObservable, computed } from "mobx"; import { action, comparer, observable, makeObservable, computed } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "../base-store";
import migrations from "../migrations/hotbar-store"; import migrations from "../../migrations/hotbar-store";
import { toJS } from "./utils"; import { toJS } from "../utils";
import type { CatalogEntity } from "./catalog"; import type { CatalogEntity } from "../catalog";
import logger from "../main/logger"; import { broadcastMessage } from "../ipc";
import { broadcastMessage } from "./ipc"; import type { Hotbar, CreateHotbarData, CreateHotbarOptions } from "./types";
import type { import { defaultHotbarCells, getEmptyHotbar } from "./types";
Hotbar, import { hotbarTooManyItemsChannel } from "../ipc/hotbar";
CreateHotbarData, import type { GeneralEntity } from "../catalog-entities";
CreateHotbarOptions } from "./hotbar-types"; import type { Logger } from "../logger";
import { import assert from "assert";
defaultHotbarCells,
getEmptyHotbar,
} from "./hotbar-types";
import { hotbarTooManyItemsChannel } from "./ipc/hotbar";
import type { GeneralEntity } from "./catalog-entities";
export interface HotbarStoreModel { export interface HotbarStoreModel {
hotbars: Hotbar[]; hotbars: Hotbar[];
@ -27,15 +22,16 @@ export interface HotbarStoreModel {
} }
interface Dependencies { interface Dependencies {
catalogCatalogEntity: GeneralEntity; readonly catalogCatalogEntity: GeneralEntity;
readonly logger: Logger;
} }
export class HotbarStore extends BaseStore<HotbarStoreModel> { export class HotbarStore extends BaseStore<HotbarStoreModel> {
readonly displayName = "HotbarStore"; readonly displayName = "HotbarStore";
@observable hotbars: Hotbar[] = []; @observable hotbars: Hotbar[] = [];
@observable private _activeHotbarId: string; @observable private _activeHotbarId!: string;
constructor(private dependencies: Dependencies) { constructor(private readonly dependencies: Dependencies) {
super({ super({
configName: "lens-hotbar-store", configName: "lens-hotbar-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
@ -62,7 +58,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
this._activeHotbarId = this.hotbars[hotbar].id; this._activeHotbarId = this.hotbars[hotbar].id;
} }
} else if (typeof hotbar === "string") { } else if (typeof hotbar === "string") {
if (this.getById(hotbar)) { if (this.findById(hotbar)) {
this._activeHotbarId = hotbar; this._activeHotbarId = hotbar;
} }
} else { } else {
@ -120,34 +116,35 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
return toJS(model); return toJS(model);
} }
getActive() { getActive(): Hotbar {
return this.getById(this.activeHotbarId); const hotbar = this.findById(this.activeHotbarId);
assert(hotbar, "There MUST always be an active hotbar");
return hotbar;
} }
getByName(name: string) { findByName(name: string) {
return this.hotbars.find((hotbar) => hotbar.name === name); return this.hotbars.find((hotbar) => hotbar.name === name);
} }
getById(id: string) { findById(id: string) {
return this.hotbars.find((hotbar) => hotbar.id === id); return this.hotbars.find((hotbar) => hotbar.id === id);
} }
add = action( @action
( add(data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) {
data: CreateHotbarData, const hotbar = getEmptyHotbar(data.name, data.id);
{ setActive = false }: CreateHotbarOptions = {},
) => {
const hotbar = getEmptyHotbar(data.name, data.id);
this.hotbars.push(hotbar); this.hotbars.push(hotbar);
if (setActive) { if (setActive) {
this._activeHotbarId = hotbar.id; this._activeHotbarId = hotbar.id;
} }
}, }
);
setHotbarName = action((id: string, name: string) => { @action
setHotbarName(id: string, name: string): void {
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id); const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
if (index < 0) { if (index < 0) {
@ -158,19 +155,18 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
} }
this.hotbars[index].name = name; this.hotbars[index].name = name;
}); }
remove = action((hotbar: Hotbar) => { @action
if (this.hotbars.length <= 1) { remove(hotbar: Hotbar) {
throw new Error("Cannot remove the last hotbar"); assert(this.hotbars.length >= 2, "Cannot remove the last hotbar");
}
this.hotbars = this.hotbars.filter((h) => h !== hotbar); this.hotbars = this.hotbars.filter((h) => h !== hotbar);
if (this.activeHotbarId === hotbar.id) { if (this.activeHotbarId === hotbar.id) {
this.setActiveHotbar(0); this.setActiveHotbar(0);
} }
}); }
@action @action
addToHotbar(item: CatalogEntity, cellIndex?: number) { addToHotbar(item: CatalogEntity, cellIndex?: number) {
@ -209,7 +205,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
} else if (0 <= cellIndex && cellIndex < hotbar.items.length) { } else if (0 <= cellIndex && cellIndex < hotbar.items.length) {
hotbar.items[cellIndex] = newItem; hotbar.items[cellIndex] = newItem;
} else { } else {
logger.error( this.dependencies.logger.error(
`[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`, `[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`,
{ entityId: uid, hotbarId: hotbar.id, cellIndex }, { entityId: uid, hotbarId: hotbar.id, cellIndex },
); );
@ -246,8 +242,9 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
findClosestEmptyIndex(from: number, direction = 1) { findClosestEmptyIndex(from: number, direction = 1) {
let index = from; let index = from;
const hotbar = this.getActive();
while (this.getActive().items[index] != null) { while (hotbar.items[index] != null) {
index += direction; index += direction;
} }
@ -314,11 +311,9 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
return false; return false;
} }
return ( const indexInActiveHotbar = this.getActive().items.findIndex(item => item?.entity.uid === entity.getId());
this.getActive().items.findIndex(
(item) => item?.entity.uid === entity.getId(), return indexInActiveHotbar >= 0;
) >= 0
);
} }
getDisplayLabel(hotbar: Hotbar): string { getDisplayLabel(hotbar: Hotbar): string {

View File

@ -4,13 +4,13 @@
*/ */
import * as uuid from "uuid"; import * as uuid from "uuid";
import type { Tuple } from "./utils"; import type { Tuple } from "../utils";
import { tuple } from "./utils"; import { tuple } from "../utils";
export interface HotbarItem { export interface HotbarItem {
entity: { entity: {
uid: string; uid: string;
name?: string; name: string;
source?: string; source?: string;
}; };
params?: { params?: {

View File

@ -6,5 +6,5 @@ import type { Channel } from "../channel";
export const createChannel = <Message>(name: string): Channel<Message> => ({ export const createChannel = <Message>(name: string): Channel<Message> => ({
name, name,
_template: null, _template: null as never,
}); });

View File

@ -53,7 +53,7 @@ describe("type enforced ipc tests", () => {
const source = new EventEmitter(); const source = new EventEmitter();
const listener = () => called += 1; const listener = () => called += 1;
const results = [true, false, true]; const results = [true, false, true];
const verifier = (args: unknown[]): args is [] => results.pop(); const verifier = (args: unknown[]): args is [] => results.pop() ?? false;
const channel = "foobar"; const channel = "foobar";
onCorrect({ source, listener, verifier, channel }); onCorrect({ source, listener, verifier, channel });

View File

@ -13,8 +13,6 @@ export interface ItemObject {
} }
export abstract class ItemStore<Item extends ItemObject> { export abstract class ItemStore<Item extends ItemObject> {
abstract loadAll(...args: any[]): Promise<void | Item[]>;
protected defaultSorting = (item: Item) => item.getName(); protected defaultSorting = (item: Item) => item.getName();
@observable failedLoading = false; @observable failedLoading = false;
@ -44,8 +42,7 @@ export abstract class ItemStore<Item extends ItemObject> {
return this.items.length; return this.items.length;
} }
getByName(name: string, ...args: any[]): Item; getByName(name: string): Item | undefined {
getByName(name: string): Item {
return this.items.find(item => item.getName() === name); return this.items.find(item => item.getName() === name);
} }
@ -115,7 +112,6 @@ export abstract class ItemStore<Item extends ItemObject> {
} }
} }
protected async loadItem(...args: any[]): Promise<Item>;
@action @action
protected async loadItem(request: () => Promise<Item>, sortItems = true) { protected async loadItem(request: () => Promise<Item>, sortItems = true) {
const item = await Promise.resolve(request()).catch(() => null); const item = await Promise.resolve(request()).catch(() => null);
@ -133,9 +129,9 @@ export abstract class ItemStore<Item extends ItemObject> {
if (sortItems) items = this.sortItems(items); if (sortItems) items = this.sortItems(items);
this.items.replace(items); this.items.replace(items);
} }
return item;
} }
return item;
} }
@action @action

View File

@ -3,19 +3,32 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { ingressStore } from "../../../renderer/components/+network-ingresses/ingress.store"; import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
import { apiManager } from "../api-manager"; import type { ApiManager } from "../api-manager";
import apiManagerInjectable from "../api-manager/manager.injectable";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeObjectStore } from "../kube-object.store";
class TestApi extends KubeApi<KubeObject> { class TestApi extends KubeApi<KubeObject> {
protected async checkPreferredVersion() { protected async checkPreferredVersion() {
return; return;
} }
} }
class TestStore extends KubeObjectStore<KubeObject, TestApi> {
}
describe("ApiManager", () => { describe("ApiManager", () => {
let apiManager: ApiManager;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
apiManager = di.inject(apiManagerInjectable);
});
describe("registerApi", () => { describe("registerApi", () => {
it("re-register store if apiBase changed", async () => { it("re-register store if apiBase changed", async () => {
const apiBase = "apis/v1/foo"; const apiBase = "apis/v1/foo";
@ -23,25 +36,27 @@ describe("ApiManager", () => {
const kubeApi = new TestApi({ const kubeApi = new TestApi({
objectConstructor: KubeObject, objectConstructor: KubeObject,
apiBase, apiBase,
kind: "foo",
fallbackApiBases: [fallbackApiBase], fallbackApiBases: [fallbackApiBase],
checkPreferredVersion: true, checkPreferredVersion: true,
}); });
const kubeStore = new TestStore(kubeApi);
apiManager.registerApi(apiBase, kubeApi); apiManager.registerApi(apiBase, kubeApi);
// Define to use test api for ingress store // Define to use test api for ingress store
Object.defineProperty(ingressStore, "api", { value: kubeApi }); Object.defineProperty(kubeStore, "api", { value: kubeApi });
apiManager.registerStore(ingressStore, [kubeApi]); apiManager.registerStore(kubeStore, [kubeApi]);
// Test that store is returned with original apiBase // Test that store is returned with original apiBase
expect(apiManager.getStore(kubeApi)).toBe(ingressStore); expect(apiManager.getStore(kubeApi)).toBe(kubeStore);
// Change apiBase similar as checkPreferredVersion does // Change apiBase similar as checkPreferredVersion does
Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase }); Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase });
apiManager.registerApi(fallbackApiBase, kubeApi); apiManager.registerApi(fallbackApiBase, kubeApi);
// Test that store is returned with new apiBase // Test that store is returned with new apiBase
expect(apiManager.getStore(kubeApi)).toBe(ingressStore); expect(apiManager.getStore(kubeApi)).toBe(kubeStore);
}); });
}); });
}); });

View File

@ -16,8 +16,15 @@ describe("Crds", () => {
name: "foo", name: "foo",
resourceVersion: "12345", resourceVersion: "12345",
uid: "12345", uid: "12345",
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
}, },
spec: { spec: {
group: "foo.bar",
names: {
kind: "Foo",
plural: "foos",
},
scope: "Namespaced",
versions: [ versions: [
{ {
name: "123", name: "123",
@ -44,8 +51,15 @@ describe("Crds", () => {
name: "foo", name: "foo",
resourceVersion: "12345", resourceVersion: "12345",
uid: "12345", uid: "12345",
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
}, },
spec: { spec: {
group: "foo.bar",
names: {
kind: "Foo",
plural: "foos",
},
scope: "Namespaced",
versions: [ versions: [
{ {
name: "123", name: "123",
@ -72,8 +86,15 @@ describe("Crds", () => {
name: "foo", name: "foo",
resourceVersion: "12345", resourceVersion: "12345",
uid: "12345", uid: "12345",
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
}, },
spec: { spec: {
group: "foo.bar",
names: {
kind: "Foo",
plural: "foos",
},
scope: "Namespaced",
versions: [ versions: [
{ {
name: "123", name: "123",
@ -100,8 +121,15 @@ describe("Crds", () => {
name: "foo", name: "foo",
resourceVersion: "12345", resourceVersion: "12345",
uid: "12345", uid: "12345",
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
}, },
spec: { spec: {
group: "foo.bar",
names: {
kind: "Foo",
plural: "foos",
},
scope: "Namespaced",
version: "abc", version: "abc",
versions: [ versions: [
{ {
@ -129,6 +157,7 @@ describe("Crds", () => {
name: "foo", name: "foo",
resourceVersion: "12345", resourceVersion: "12345",
uid: "12345", uid: "12345",
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
}, },
spec: { spec: {
version: "abc", version: "abc",

View File

@ -3,31 +3,39 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { Deployment, DeploymentApi } from "../endpoints/deployment.api"; import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
import apiKubeInjectable from "../../../renderer/k8s/api-kube.injectable";
import type { DeploymentApi } from "../endpoints/deployment.api";
import deploymentApiInjectable from "../endpoints/deployment.api.injectable";
import type { KubeJsonApi } from "../kube-json-api"; import type { KubeJsonApi } from "../kube-json-api";
class DeploymentApiTest extends DeploymentApi {
public setRequest(request: any) {
this.request = request;
}
}
describe("DeploymentApi", () => { describe("DeploymentApi", () => {
let deploymentApi: DeploymentApi;
let kubeJsonApi: jest.Mocked<KubeJsonApi>;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(storesAndApisCanBeCreatedInjectable, () => true);
kubeJsonApi = {
getResponse: jest.fn(),
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
patch: jest.fn(),
del: jest.fn(),
} as never;
di.override(apiKubeInjectable, () => kubeJsonApi);
deploymentApi = di.inject(deploymentApiInjectable);
});
describe("scale", () => { describe("scale", () => {
const requestMock = {
patch: () => ({}),
} as unknown as KubeJsonApi;
const sub = new DeploymentApiTest({ objectConstructor: Deployment });
sub.setRequest(requestMock);
it("requests Kubernetes API with PATCH verb and correct amount of replicas", () => { it("requests Kubernetes API with PATCH verb and correct amount of replicas", () => {
const patchSpy = jest.spyOn(requestMock, "patch"); deploymentApi.scale({ namespace: "default", name: "deployment-1" }, 5);
sub.scale({ namespace: "default", name: "deployment-1" }, 5); expect(kubeJsonApi.patch).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/deployments/deployment-1/scale", {
expect(patchSpy).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/deployments/deployment-1/scale", {
data: { data: {
spec: { spec: {
replicas: 5, replicas: 5,

View File

@ -3,42 +3,26 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { EndpointSubset } from "../endpoints"; import { formatEndpointSubset } from "../endpoints";
describe("endpoint tests", () => { describe("endpoint tests", () => {
describe("EndpointSubset", () => { describe("EndpointSubset", () => {
it.each([ it("formatEndpointSubset should be addresses X ports", () => {
4, const formatted = formatEndpointSubset({
false,
null,
{},
[],
"ahe",
/a/,
])("should always initialize fields when given %j", (data: any) => {
const sub = new EndpointSubset(data);
expect(sub.addresses).toStrictEqual([]);
expect(sub.notReadyAddresses).toStrictEqual([]);
expect(sub.ports).toStrictEqual([]);
});
it("toString should be addresses X ports", () => {
const sub = new EndpointSubset({
addresses: [{ addresses: [{
ip: "1.1.1.1", ip: "1.1.1.1",
}, { }, {
ip: "1.1.1.2", ip: "1.1.1.2",
}] as any, }],
notReadyAddresses: [], notReadyAddresses: [],
ports: [{ ports: [{
port: "81", port: 81,
}, { }, {
port: "82", port: 82,
}] as any, }],
}); });
expect(sub.toString()).toBe("1.1.1.1:81, 1.1.1.1:82, 1.1.1.2:81, 1.1.1.2:82"); expect(formatted).toBe("1.1.1.1:81, 1.1.1.1:82, 1.1.1.2:81, 1.1.1.2:82");
}); });
}); });
}); });

View File

@ -9,33 +9,33 @@ import { HelmChart } from "../endpoints/helm-charts.api";
describe("HelmChart tests", () => { describe("HelmChart tests", () => {
describe("HelmChart.create() tests", () => { describe("HelmChart.create() tests", () => {
it("should throw on non-object input", () => { it("should throw on non-object input", () => {
expect(() => HelmChart.create("" as any)).toThrowError('"value" must be of type object'); expect(() => HelmChart.create("" as never)).toThrowError('"value" must be of type object');
expect(() => HelmChart.create(1 as any)).toThrowError('"value" must be of type object'); expect(() => HelmChart.create(1 as never)).toThrowError('"value" must be of type object');
expect(() => HelmChart.create(false as any)).toThrowError('"value" must be of type object'); expect(() => HelmChart.create(false as never)).toThrowError('"value" must be of type object');
expect(() => HelmChart.create([] as any)).toThrowError('"value" must be of type object'); expect(() => HelmChart.create([] as never)).toThrowError('"value" must be of type object');
expect(() => HelmChart.create(Symbol() as any)).toThrowError('"value" must be of type object'); expect(() => HelmChart.create(Symbol() as never)).toThrowError('"value" must be of type object');
}); });
it("should throw on missing fields", () => { it("should throw on missing fields", () => {
expect(() => HelmChart.create({} as any)).toThrowError('"apiVersion" is required'); expect(() => HelmChart.create({} as never)).toThrowError('"apiVersion" is required');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "!", apiVersion: "!",
} as any)).toThrowError('"name" is required'); } as never)).toThrowError('"name" is required');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "!", apiVersion: "!",
name: "!", name: "!",
} as any)).toThrowError('"version" is required'); } as never)).toThrowError('"version" is required');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "!", apiVersion: "!",
name: "!", name: "!",
version: "!", version: "!",
} as any)).toThrowError('"repo" is required'); } as never)).toThrowError('"repo" is required');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "!", apiVersion: "!",
name: "!", name: "!",
version: "!", version: "!",
repo: "!", repo: "!",
} as any)).toThrowError('"created" is required'); } as never)).toThrowError('"created" is required');
}); });
it("should throw on fields being wrong type", () => { it("should throw on fields being wrong type", () => {
@ -46,7 +46,7 @@ describe("HelmChart tests", () => {
repo: "!", repo: "!",
created: "!", created: "!",
digest: "!", digest: "!",
} as any)).toThrowError('"apiVersion" must be a string'); } as never)).toThrowError('"apiVersion" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: 1, name: 1,
@ -54,7 +54,7 @@ describe("HelmChart tests", () => {
repo: "!", repo: "!",
created: "!", created: "!",
digest: "!", digest: "!",
} as any)).toThrowError('"name" must be a string'); } as never)).toThrowError('"name" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "!", apiVersion: "!",
name: "!", name: "!",
@ -62,7 +62,7 @@ describe("HelmChart tests", () => {
repo: "!", repo: "!",
created: "!", created: "!",
digest: 1, digest: 1,
} as any)).toThrowError('"digest" must be a string'); } as never)).toThrowError('"digest" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "", name: "",
@ -70,7 +70,7 @@ describe("HelmChart tests", () => {
repo: "!", repo: "!",
created: "!", created: "!",
digest: "!", digest: "!",
} as any)).toThrowError('"version" must be a string'); } as never)).toThrowError('"version" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -78,7 +78,7 @@ describe("HelmChart tests", () => {
repo: 1, repo: 1,
created: "!", created: "!",
digest: "!", digest: "!",
} as any)).toThrowError('"repo" must be a string'); } as never)).toThrowError('"repo" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -86,7 +86,7 @@ describe("HelmChart tests", () => {
repo: "1", repo: "1",
created: 1, created: 1,
digest: "a", digest: "a",
} as any)).toThrowError('"created" must be a string'); } as never)).toThrowError('"created" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -94,7 +94,7 @@ describe("HelmChart tests", () => {
repo: "1", repo: "1",
created: "!", created: "!",
digest: 1, digest: 1,
} as any)).toThrowError('"digest" must be a string'); } as never)).toThrowError('"digest" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -103,7 +103,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
kubeVersion: 1, kubeVersion: 1,
} as any)).toThrowError('"kubeVersion" must be a string'); } as never)).toThrowError('"kubeVersion" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -112,7 +112,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
description: 1, description: 1,
} as any)).toThrowError('"description" must be a string'); } as never)).toThrowError('"description" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -121,7 +121,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
home: 1, home: 1,
} as any)).toThrowError('"home" must be a string'); } as never)).toThrowError('"home" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -130,7 +130,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
engine: 1, engine: 1,
} as any)).toThrowError('"engine" must be a string'); } as never)).toThrowError('"engine" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -139,7 +139,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
icon: 1, icon: 1,
} as any)).toThrowError('"icon" must be a string'); } as never)).toThrowError('"icon" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -148,7 +148,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
appVersion: 1, appVersion: 1,
} as any)).toThrowError('"appVersion" must be a string'); } as never)).toThrowError('"appVersion" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -157,7 +157,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
tillerVersion: 1, tillerVersion: 1,
} as any)).toThrowError('"tillerVersion" must be a string'); } as never)).toThrowError('"tillerVersion" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -166,7 +166,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
deprecated: 1, deprecated: 1,
} as any)).toThrowError('"deprecated" must be a boolean'); } as never)).toThrowError('"deprecated" must be a boolean');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -175,7 +175,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
keywords: 1, keywords: 1,
} as any)).toThrowError('"keywords" must be an array'); } as never)).toThrowError('"keywords" must be an array');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -184,7 +184,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
sources: 1, sources: 1,
} as any)).toThrowError('"sources" must be an array'); } as never)).toThrowError('"sources" must be an array');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -193,7 +193,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
maintainers: 1, maintainers: 1,
} as any)).toThrowError('"maintainers" must be an array'); } as never)).toThrowError('"maintainers" must be an array');
}); });
it("should filter non-string keywords", () => { it("should filter non-string keywords", () => {
@ -204,10 +204,10 @@ describe("HelmChart tests", () => {
repo: "1", repo: "1",
digest: "1", digest: "1",
created: "!", created: "!",
keywords: [1, "a", false, {}, "b"] as any, keywords: [1, "a", false, {}, "b"] as never,
}); });
expect(chart.keywords).toStrictEqual(["a", "b"]); expect(chart?.keywords).toStrictEqual(["a", "b"]);
}); });
it("should filter non-string sources", () => { it("should filter non-string sources", () => {
@ -218,10 +218,10 @@ describe("HelmChart tests", () => {
repo: "1", repo: "1",
digest: "1", digest: "1",
created: "!", created: "!",
sources: [1, "a", false, {}, "b"] as any, sources: [1, "a", false, {}, "b"] as never,
}); });
expect(chart.sources).toStrictEqual(["a", "b"]); expect(chart?.sources).toStrictEqual(["a", "b"]);
}); });
it("should filter invalid maintainers", () => { it("should filter invalid maintainers", () => {
@ -236,10 +236,10 @@ describe("HelmChart tests", () => {
name: "a", name: "a",
email: "b", email: "b",
url: "c", url: "c",
}] as any, }] as never,
}); });
expect(chart.maintainers).toStrictEqual([{ expect(chart?.maintainers).toStrictEqual([{
name: "a", name: "a",
email: "b", email: "b",
url: "c", url: "c",
@ -261,9 +261,9 @@ describe("HelmChart tests", () => {
name: "a", name: "a",
email: "b", email: "b",
url: "c", url: "c",
}] as any, }] as never,
"asdjhajksdhadjks": 1, "asdjhajksdhadjks": 1,
} as any); } as never);
expect(warnFn).toHaveBeenCalledWith("HelmChart data has unexpected fields", { expect(warnFn).toHaveBeenCalledWith("HelmChart data has unexpected fields", {
original: anyObject(), original: anyObject(),

View File

@ -14,6 +14,8 @@ describe("computeRuleDeclarations", () => {
name: "foo", name: "foo",
resourceVersion: "1", resourceVersion: "1",
uid: "bar", uid: "bar",
namespace: "default",
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
}, },
}); });
@ -21,6 +23,7 @@ describe("computeRuleDeclarations", () => {
host: "foo.bar", host: "foo.bar",
http: { http: {
paths: [{ paths: [{
pathType: "Exact",
backend: { backend: {
service: { service: {
name: "my-service", name: "my-service",
@ -44,6 +47,8 @@ describe("computeRuleDeclarations", () => {
name: "foo", name: "foo",
resourceVersion: "1", resourceVersion: "1",
uid: "bar", uid: "bar",
namespace: "default",
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
}, },
}); });
@ -55,6 +60,7 @@ describe("computeRuleDeclarations", () => {
host: "foo.bar", host: "foo.bar",
http: { http: {
paths: [{ paths: [{
pathType: "Exact",
backend: { backend: {
service: { service: {
name: "my-service", name: "my-service",
@ -78,6 +84,8 @@ describe("computeRuleDeclarations", () => {
name: "foo", name: "foo",
resourceVersion: "1", resourceVersion: "1",
uid: "bar", uid: "bar",
namespace: "default",
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
}, },
}); });
@ -91,6 +99,7 @@ describe("computeRuleDeclarations", () => {
host: "foo.bar", host: "foo.bar",
http: { http: {
paths: [{ paths: [{
pathType: "Exact",
backend: { backend: {
service: { service: {
name: "my-service", name: "my-service",

View File

@ -19,7 +19,7 @@ import { parseKubeApi } from "../kube-api-parse";
/** /**
* [<input-url>, <expected-result>] * [<input-url>, <expected-result>]
*/ */
type KubeApiParseTestData = [string, Required<IKubeApiParsed>]; type KubeApiParseTestData = [string, IKubeApiParsed];
const tests: KubeApiParseTestData[] = [ const tests: KubeApiParseTestData[] = [
["/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", { ["/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", {
@ -126,6 +126,6 @@ describe("parseApi unit tests", () => {
}); });
it.each(throwtests)("testing %j should throw", (url) => { it.each(throwtests)("testing %j should throw", (url) => {
expect(() => parseKubeApi(url)).toThrowError("invalid apiPath"); expect(() => parseKubeApi(url as never)).toThrowError("invalid apiPath");
}); });
}); });

View File

@ -3,34 +3,36 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { Request } from "node-fetch";
import { forRemoteCluster, KubeApi } from "../kube-api"; import { forRemoteCluster, KubeApi } from "../kube-api";
import { KubeJsonApi } from "../kube-json-api"; import { KubeJsonApi } from "../kube-json-api";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import AbortController from "abort-controller"; import AbortController from "abort-controller";
import { delay } from "../../utils/delay"; import { delay } from "../../utils/delay";
import { PassThrough } from "stream"; import { PassThrough } from "stream";
import type { ApiManager } from "../api-manager"; import { ApiManager } from "../api-manager";
import { apiManager } from "../api-manager"; import type { FetchMock } from "jest-fetch-mock/types";
import { Ingress, Pod } from "../endpoints"; import { DeploymentApi, Ingress, IngressApi, Pod, PodApi } from "../endpoints";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
import apiManagerInjectable from "../api-manager/manager.injectable";
import autoRegistrationInjectable from "../api-manager/auto-registration.injectable";
jest.mock("../api-manager"); jest.mock("../api-manager");
const mockApiManager = apiManager as jest.Mocked<ApiManager>; const mockFetch = fetch as FetchMock;
class TestKubeObject extends KubeObject {
static kind = "Pod";
static namespaced = true;
static apiBase = "/api/v1/pods";
}
class TestKubeApi extends KubeApi<TestKubeObject> {
public async checkPreferredVersion() {
return super.checkPreferredVersion();
}
}
describe("forRemoteCluster", () => { describe("forRemoteCluster", () => {
let apiManager: jest.Mocked<ApiManager>;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
await di.runSetups();
apiManager = new ApiManager() as jest.Mocked<ApiManager>;
di.override(apiManagerInjectable, () => apiManager);
});
it("builds api client for KubeObject", async () => { it("builds api client for KubeObject", async () => {
const api = forRemoteCluster({ const api = forRemoteCluster({
cluster: { cluster: {
@ -39,7 +41,7 @@ describe("forRemoteCluster", () => {
user: { user: {
token: "daa", token: "daa",
}, },
}, TestKubeObject); }, Pod);
expect(api).toBeInstanceOf(KubeApi); expect(api).toBeInstanceOf(KubeApi);
}); });
@ -52,9 +54,9 @@ describe("forRemoteCluster", () => {
user: { user: {
token: "daa", token: "daa",
}, },
}, TestKubeObject, TestKubeApi); }, Pod, PodApi);
expect(api).toBeInstanceOf(TestKubeApi); expect(api).toBeInstanceOf(PodApi);
}); });
it("calls right api endpoint", async () => { it("calls right api endpoint", async () => {
@ -65,9 +67,9 @@ describe("forRemoteCluster", () => {
user: { user: {
token: "daa", token: "daa",
}, },
}, TestKubeObject); }, Pod);
(fetch as any).mockResponse(async (request: any) => { mockFetch.mockResponse(async (request: any) => {
expect(request.url).toEqual("https://127.0.0.1:6443/api/v1/pods"); expect(request.url).toEqual("https://127.0.0.1:6443/api/v1/pods");
return { return {
@ -83,22 +85,31 @@ describe("forRemoteCluster", () => {
describe("KubeApi", () => { describe("KubeApi", () => {
let request: KubeJsonApi; let request: KubeJsonApi;
let apiManager: jest.Mocked<ApiManager>;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
await di.runSetups();
beforeEach(() => {
request = new KubeJsonApi({ request = new KubeJsonApi({
serverAddress: `http://127.0.0.1:9999`, serverAddress: `http://127.0.0.1:9999`,
apiBase: "/api-kube", apiBase: "/api-kube",
}); });
apiManager = new ApiManager() as jest.Mocked<ApiManager>;
di.override(apiManagerInjectable, () => apiManager);
di.inject(autoRegistrationInjectable);
}); });
it("uses url from apiBase if apiBase contains the resource", async () => { it("uses url from apiBase if apiBase contains the resource", async () => {
(fetch as any).mockResponse(async (request: any) => { mockFetch.mockResponse(async (request: any) => {
if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") { if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
return { return {
body: JSON.stringify({ body: JSON.stringify({
resources: [{ resources: [{
name: "ingresses", name: "ingresses",
}] as any[], }],
}), }),
}; };
} else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") { } else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
@ -107,13 +118,13 @@ describe("KubeApi", () => {
body: JSON.stringify({ body: JSON.stringify({
resources: [{ resources: [{
name: "ingresses", name: "ingresses",
}] as any[], }],
}), }),
}; };
} else { } else {
return { return {
body: JSON.stringify({ body: JSON.stringify({
resources: [] as any[], resources: [],
}), }),
}; };
} }
@ -121,9 +132,9 @@ describe("KubeApi", () => {
const apiBase = "/apis/networking.k8s.io/v1/ingresses"; const apiBase = "/apis/networking.k8s.io/v1/ingresses";
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses"; const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
const kubeApi = new KubeApi({ const kubeApi = new IngressApi({
request, request,
objectConstructor: KubeObject, objectConstructor: Ingress,
apiBase, apiBase,
fallbackApiBases: [fallbackApiBase], fallbackApiBases: [fallbackApiBase],
checkPreferredVersion: true, checkPreferredVersion: true,
@ -138,11 +149,11 @@ describe("KubeApi", () => {
}); });
it("uses url from fallbackApiBases if apiBase lacks the resource", async () => { it("uses url from fallbackApiBases if apiBase lacks the resource", async () => {
(fetch as any).mockResponse(async (request: any) => { mockFetch.mockResponse(async (request: any) => {
if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") { if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
return { return {
body: JSON.stringify({ body: JSON.stringify({
resources: [] as any[], resources: [],
}), }),
}; };
} else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") { } else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
@ -150,13 +161,13 @@ describe("KubeApi", () => {
body: JSON.stringify({ body: JSON.stringify({
resources: [{ resources: [{
name: "ingresses", name: "ingresses",
}] as any[], }],
}), }),
}; };
} else { } else {
return { return {
body: JSON.stringify({ body: JSON.stringify({
resources: [] as any[], resources: [],
}), }),
}; };
} }
@ -164,9 +175,10 @@ describe("KubeApi", () => {
const apiBase = "apis/networking.k8s.io/v1/ingresses"; const apiBase = "apis/networking.k8s.io/v1/ingresses";
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses"; const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
const kubeApi = new KubeApi({ const kubeApi = new IngressApi({
request, request,
objectConstructor: Object.assign(KubeObject, { apiBase }), objectConstructor: Object.assign(KubeObject, { apiBase }),
kind: "Ingress",
fallbackApiBases: [fallbackApiBase], fallbackApiBases: [fallbackApiBase],
checkPreferredVersion: true, checkPreferredVersion: true,
}); });
@ -183,107 +195,100 @@ describe("KubeApi", () => {
it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred", async () => { it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred", async () => {
expect.hasAssertions(); expect.hasAssertions();
const api = new TestKubeApi({ const api = new IngressApi({
objectConstructor: Ingress, objectConstructor: Ingress,
checkPreferredVersion: true, checkPreferredVersion: true,
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"], fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
request: { request: {
get: jest.fn() get: jest.fn()
.mockImplementationOnce((path: string) => { .mockImplementation((path: string) => {
expect(path).toBe("/apis/networking.k8s.io/v1"); switch (path) {
case "/apis/networking.k8s.io/v1":
throw new Error("no"); throw new Error("no");
}) case "/apis/extensions/v1beta1":
.mockImplementationOnce((path: string) => { return {
expect(path).toBe("/apis/extensions/v1beta1"); resources: [
{
return { name: "ingresses",
resources: [ },
{ ],
name: "ingresses", };
}, case "/apis/extensions":
], return {
}; preferredVersion: {
}) version: "v1beta1",
.mockImplementationOnce((path: string) => { },
expect(path).toBe("/apis/extensions"); };
default:
return { throw new Error("unknown path");
preferredVersion: { }
version: "v1beta1",
},
};
}), }),
} as any, } as Partial<KubeJsonApi> as KubeJsonApi,
}); });
await api.checkPreferredVersion(); await (api as any).checkPreferredVersion();
expect(api.apiVersionPreferred).toBe("v1beta1"); expect(api.apiVersionPreferred).toBe("v1beta1");
expect(mockApiManager.registerApi).toBeCalledWith("/apis/extensions/v1beta1/ingresses", expect.anything()); expect(apiManager.registerApi).toBeCalledWith(api);
}); });
it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred with non-grouped apis", async () => { it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred with non-grouped apis", async () => {
expect.hasAssertions(); expect.hasAssertions();
const api = new TestKubeApi({ const api = new PodApi({
objectConstructor: Pod, objectConstructor: Pod,
checkPreferredVersion: true, checkPreferredVersion: true,
fallbackApiBases: ["/api/v1beta1/pods"], fallbackApiBases: ["/api/v1beta1/pods"],
request: { request: {
get: jest.fn() get: jest.fn()
.mockImplementationOnce((path: string) => { .mockImplementation((path: string) => {
expect(path).toBe("/api/v1"); switch (path) {
case "/api/v1":
throw new Error("no"); throw new Error("no");
}) case "/api/v1beta1":
.mockImplementationOnce((path: string) => { return {
expect(path).toBe("/api/v1beta1"); resources: [
{
return { name: "pods",
resources: [ },
{ ],
name: "pods", };
}, case "/api":
], return {
}; preferredVersion: {
}) version: "v1beta1",
.mockImplementationOnce((path: string) => { },
expect(path).toBe("/api"); };
default:
return { throw new Error("unknown path");
preferredVersion: { }
version: "v1beta1",
},
};
}), }),
} as any, } as Partial<KubeJsonApi> as KubeJsonApi,
}); });
await api.checkPreferredVersion(); await (api as any).checkPreferredVersion();
expect(api.apiVersionPreferred).toBe("v1beta1"); expect(api.apiVersionPreferred).toBe("v1beta1");
expect(mockApiManager.registerApi).toBeCalledWith("/api/v1beta1/pods", expect.anything()); expect(apiManager.registerApi).toBeCalledWith(api);
}); });
}); });
describe("patch", () => { describe("patch", () => {
let api: TestKubeApi; let api: DeploymentApi;
beforeEach(() => { beforeEach(() => {
api = new TestKubeApi({ api = new DeploymentApi({
request, request,
objectConstructor: TestKubeObject,
}); });
}); });
it("sends strategic patch by default", async () => { it("sends strategic patch by default", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PATCH"); expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("strategic-merge-patch"); expect(request.headers.get("content-type")).toMatch("strategic-merge-patch");
expect(request.body.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }})); expect(request.body?.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
return {}; return {};
}); });
@ -296,10 +301,10 @@ describe("KubeApi", () => {
it("allows to use merge patch", async () => { it("allows to use merge patch", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PATCH"); expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("merge-patch"); expect(request.headers.get("content-type")).toMatch("merge-patch");
expect(request.body.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }})); expect(request.body?.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
return {}; return {};
}); });
@ -312,10 +317,10 @@ describe("KubeApi", () => {
it("allows to use json patch", async () => { it("allows to use json patch", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PATCH"); expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("json-patch"); expect(request.headers.get("content-type")).toMatch("json-patch");
expect(request.body.toString()).toEqual(JSON.stringify([{ op: "replace", path: "/spec/replicas", value: 2 }])); expect(request.body?.toString()).toEqual(JSON.stringify([{ op: "replace", path: "/spec/replicas", value: 2 }]));
return {}; return {};
}); });
@ -328,10 +333,10 @@ describe("KubeApi", () => {
it("allows deep partial patch", async () => { it("allows deep partial patch", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PATCH"); expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("merge-patch"); expect(request.headers.get("content-type")).toMatch("merge-patch");
expect(request.body.toString()).toEqual(JSON.stringify({ metadata: { annotations: { provisioned: "true" }}})); expect(request.body?.toString()).toEqual(JSON.stringify({ metadata: { annotations: { provisioned: "true" }}}));
return {}; return {};
}); });
@ -345,18 +350,18 @@ describe("KubeApi", () => {
}); });
describe("delete", () => { describe("delete", () => {
let api: TestKubeApi; let api: PodApi;
beforeEach(() => { beforeEach(() => {
api = new TestKubeApi({ api = new PodApi({
request, request,
objectConstructor: TestKubeObject, objectConstructor: Pod,
}); });
}); });
it("sends correct request with empty namespace", async () => { it("sends correct request with empty namespace", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("DELETE"); expect(request.method).toEqual("DELETE");
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/pods/foo?propagationPolicy=Background"); expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/pods/foo?propagationPolicy=Background");
@ -368,7 +373,7 @@ describe("KubeApi", () => {
it("sends correct request without namespace", async () => { it("sends correct request without namespace", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("DELETE"); expect(request.method).toEqual("DELETE");
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background"); expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background");
@ -380,7 +385,7 @@ describe("KubeApi", () => {
it("sends correct request with namespace", async () => { it("sends correct request with namespace", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("DELETE"); expect(request.method).toEqual("DELETE");
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods/foo?propagationPolicy=Background"); expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods/foo?propagationPolicy=Background");
@ -392,7 +397,7 @@ describe("KubeApi", () => {
it("allows to change propagationPolicy", async () => { it("allows to change propagationPolicy", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("DELETE"); expect(request.method).toEqual("DELETE");
expect(request.url).toMatch("propagationPolicy=Orphan"); expect(request.url).toMatch("propagationPolicy=Orphan");
@ -404,13 +409,13 @@ describe("KubeApi", () => {
}); });
describe("watch", () => { describe("watch", () => {
let api: TestKubeApi; let api: PodApi;
let stream: PassThrough; let stream: PassThrough;
beforeEach(() => { beforeEach(() => {
api = new TestKubeApi({ api = new PodApi({
request, request,
objectConstructor: TestKubeObject, objectConstructor: Pod,
}); });
stream = new PassThrough(); stream = new PassThrough();
}); });
@ -423,9 +428,10 @@ describe("KubeApi", () => {
it("sends a valid watch request", () => { it("sends a valid watch request", () => {
const spy = jest.spyOn(request, "getResponse"); const spy = jest.spyOn(request, "getResponse");
(fetch as any).mockResponse(async () => { mockFetch.mockResponse(async () => {
return { return {
body: stream, // needed for https://github.com/jefflau/jest-fetch-mock/issues/218
body: stream as unknown as string,
}; };
}); });
@ -436,9 +442,10 @@ describe("KubeApi", () => {
it("sends timeout as a query parameter", async () => { it("sends timeout as a query parameter", async () => {
const spy = jest.spyOn(request, "getResponse"); const spy = jest.spyOn(request, "getResponse");
(fetch as any).mockResponse(async () => { mockFetch.mockResponse(async () => {
return { return {
body: stream, // needed for https://github.com/jefflau/jest-fetch-mock/issues/218
body: stream as unknown as string,
}; };
}); });
@ -449,13 +456,14 @@ describe("KubeApi", () => {
it("aborts watch using abortController", async (done) => { it("aborts watch using abortController", async (done) => {
const spy = jest.spyOn(request, "getResponse"); const spy = jest.spyOn(request, "getResponse");
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
(request as any).signal.addEventListener("abort", () => { request.signal.addEventListener("abort", () => {
done(); done();
}); });
return { return {
body: stream, // needed for https://github.com/jefflau/jest-fetch-mock/issues/218
body: stream as unknown as string,
}; };
}); });
@ -478,9 +486,9 @@ describe("KubeApi", () => {
it("if request ended", (done) => { it("if request ended", (done) => {
const spy = jest.spyOn(request, "getResponse"); const spy = jest.spyOn(request, "getResponse");
jest.spyOn(stream, "on").mockImplementation((eventName: string, callback: Function) => { jest.spyOn(stream, "on").mockImplementation((event: string | symbol, callback: Function) => {
// End the request in 100ms. // End the request in 100ms.
if (eventName === "end") { if (event === "end") {
setTimeout(() => { setTimeout(() => {
callback(); callback();
}, 100); }, 100);
@ -493,8 +501,8 @@ describe("KubeApi", () => {
jest.spyOn(global, "fetch").mockImplementation(async () => { jest.spyOn(global, "fetch").mockImplementation(async () => {
return { return {
ok: true, ok: true,
body: stream, body: stream as never,
} as any; } as Partial<Response> as Response;
}); });
api.watch({ api.watch({
@ -512,9 +520,10 @@ describe("KubeApi", () => {
it("if request not closed after timeout", (done) => { it("if request not closed after timeout", (done) => {
const spy = jest.spyOn(request, "getResponse"); const spy = jest.spyOn(request, "getResponse");
(fetch as any).mockResponse(async () => { mockFetch.mockResponse(async () => {
return { return {
body: stream, // needed for https://github.com/jefflau/jest-fetch-mock/issues/218
body: stream as unknown as string,
}; };
}); });
@ -536,9 +545,9 @@ describe("KubeApi", () => {
it("retries only once if request ends and timeout is set", (done) => { it("retries only once if request ends and timeout is set", (done) => {
const spy = jest.spyOn(request, "getResponse"); const spy = jest.spyOn(request, "getResponse");
jest.spyOn(stream, "on").mockImplementation((eventName: string, callback: Function) => { jest.spyOn(stream, "on").mockImplementation((event: string | symbol, callback: Function) => {
// End the request in 100ms. // End the request in 100ms.
if (eventName === "end") { if (event === "end") {
setTimeout(() => { setTimeout(() => {
callback(); callback();
}, 100); }, 100);
@ -551,8 +560,8 @@ describe("KubeApi", () => {
jest.spyOn(global, "fetch").mockImplementation(async () => { jest.spyOn(global, "fetch").mockImplementation(async () => {
return { return {
ok: true, ok: true,
body: stream, body: stream as never,
} as any; } as Partial<Response> as Response;
}); });
const timeoutSeconds = 0.5; const timeoutSeconds = 0.5;
@ -577,21 +586,21 @@ describe("KubeApi", () => {
}); });
describe("create", () => { describe("create", () => {
let api: TestKubeApi; let api: PodApi;
beforeEach(() => { beforeEach(() => {
api = new TestKubeApi({ api = new PodApi({
request, request,
objectConstructor: TestKubeObject, objectConstructor: Pod,
}); });
}); });
it("should add kind and apiVersion", async () => { it("should add kind and apiVersion", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("POST"); expect(request.method).toEqual("POST");
expect(JSON.parse(request.body.toString())).toEqual({ expect(JSON.parse(String(request.body))).toEqual({
kind: "Pod", kind: "Pod",
apiVersion: "v1", apiVersion: "v1",
metadata: { metadata: {
@ -643,9 +652,9 @@ describe("KubeApi", () => {
it("doesn't override metadata.labels", async () => { it("doesn't override metadata.labels", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("POST"); expect(request.method).toEqual("POST");
expect(JSON.parse(request.body.toString())).toEqual({ expect(JSON.parse(String(request.body))).toEqual({
kind: "Pod", kind: "Pod",
apiVersion: "v1", apiVersion: "v1",
metadata: { metadata: {
@ -674,21 +683,21 @@ describe("KubeApi", () => {
}); });
describe("update", () => { describe("update", () => {
let api: TestKubeApi; let api: PodApi;
beforeEach(() => { beforeEach(() => {
api = new TestKubeApi({ api = new PodApi({
request, request,
objectConstructor: TestKubeObject, objectConstructor: Pod,
}); });
}); });
it("doesn't override metadata.labels", async () => { it("doesn't override metadata.labels", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PUT"); expect(request.method).toEqual("PUT");
expect(JSON.parse(request.body.toString())).toEqual({ expect(JSON.parse(String(request.body))).toEqual({
metadata: { metadata: {
name: "foobar", name: "foobar",
namespace: "default", namespace: "default",

View File

@ -3,6 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { Cluster } from "../../cluster/cluster";
import type { ClusterContext } from "../cluster-context"; import type { ClusterContext } from "../cluster-context";
import type { KubeApi } from "../kube-api"; import type { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
@ -14,6 +15,7 @@ class FakeKubeObjectStore extends KubeObjectStore<KubeObject> {
allNamespaces: [], allNamespaces: [],
contextNamespaces: [], contextNamespaces: [],
hasSelectedAll: false, hasSelectedAll: false,
cluster: {} as Cluster,
} as ClusterContext; } as ClusterContext;
get context() { get context() {
@ -40,6 +42,7 @@ describe("KubeObjectStore", () => {
resourceVersion: "1", resourceVersion: "1",
uid: "some-uid", uid: "some-uid",
namespace: "default", namespace: "default",
selfLink: "/some/self/link",
}, },
}); });
const store = new FakeKubeObjectStore(loadItems, { const store = new FakeKubeObjectStore(loadItems, {
@ -73,6 +76,7 @@ describe("KubeObjectStore", () => {
resourceVersion: "1", resourceVersion: "1",
uid: "some-uid", uid: "some-uid",
namespace: "default", namespace: "default",
selfLink: "/some/self/link",
}, },
}); });
const objNotInDefaultNamespace = new KubeObject({ const objNotInDefaultNamespace = new KubeObject({
@ -83,6 +87,7 @@ describe("KubeObjectStore", () => {
resourceVersion: "1", resourceVersion: "1",
uid: "some-uid", uid: "some-uid",
namespace: "not-default", namespace: "not-default",
selfLink: "/some/self/link",
}, },
}); });
const store = new FakeKubeObjectStore(loadItems, { const store = new FakeKubeObjectStore(loadItems, {
@ -115,6 +120,7 @@ describe("KubeObjectStore", () => {
name: "some-obj-name", name: "some-obj-name",
resourceVersion: "1", resourceVersion: "1",
uid: "some-uid", uid: "some-uid",
selfLink: "/some/self/link",
}, },
}); });
const clusterScopedObject2 = new KubeObject({ const clusterScopedObject2 = new KubeObject({
@ -125,6 +131,7 @@ describe("KubeObjectStore", () => {
resourceVersion: "1", resourceVersion: "1",
uid: "some-uid", uid: "some-uid",
namespace: "not-default", namespace: "not-default",
selfLink: "/some/self/link",
}, },
}); });
const store = new FakeKubeObjectStore(loadItems, { const store = new FakeKubeObjectStore(loadItems, {

View File

@ -18,6 +18,7 @@ describe("Nodes tests", () => {
name: "bar", name: "bar",
resourceVersion: "1", resourceVersion: "1",
uid: "bat", uid: "bat",
selfLink: "/api/v1/nodes/bar",
}, },
}); });
@ -33,6 +34,7 @@ describe("Nodes tests", () => {
resourceVersion: "1", resourceVersion: "1",
uid: "bat", uid: "bat",
labels: {}, labels: {},
selfLink: "/api/v1/nodes/bar",
}, },
}); });
@ -51,6 +53,7 @@ describe("Nodes tests", () => {
"node-role.kubernetes.io/foobar": "bat", "node-role.kubernetes.io/foobar": "bat",
"hellonode-role.kubernetes.io/foobar1": "bat", "hellonode-role.kubernetes.io/foobar1": "bat",
}, },
selfLink: "/api/v1/nodes/bar",
}, },
}); });
@ -69,6 +72,7 @@ describe("Nodes tests", () => {
"node-role.kubernetes.io/foobar": "bat", "node-role.kubernetes.io/foobar": "bat",
"hellonode-role.kubernetes.io//////foobar1": "bat", "hellonode-role.kubernetes.io//////foobar1": "bat",
}, },
selfLink: "/api/v1/nodes/bar",
}, },
}); });
@ -86,6 +90,7 @@ describe("Nodes tests", () => {
labels: { labels: {
"kubernetes.io/role": "master", "kubernetes.io/role": "master",
}, },
selfLink: "/api/v1/nodes/bar",
}, },
}); });
@ -103,6 +108,7 @@ describe("Nodes tests", () => {
labels: { labels: {
"node.kubernetes.io/role": "master", "node.kubernetes.io/role": "master",
}, },
selfLink: "/api/v1/nodes/bar",
}, },
}); });
@ -122,6 +128,7 @@ describe("Nodes tests", () => {
"kubernetes.io/role": "master", "kubernetes.io/role": "master",
"node.kubernetes.io/role": "master-v2-max", "node.kubernetes.io/role": "master-v2-max",
}, },
selfLink: "/api/v1/nodes/bar",
}, },
}); });

View File

@ -14,6 +14,8 @@ describe("Pod tests", () => {
name: "foobar", name: "foobar",
resourceVersion: "foobar", resourceVersion: "foobar",
uid: "foobar", uid: "foobar",
namespace: "default",
selfLink: "/api/v1/pods/default/foobar",
}, },
}); });

View File

@ -3,6 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import assert from "assert";
import type { PodContainer, PodContainerStatus } from "../endpoints";
import { Pod } from "../endpoints"; import { Pod } from "../endpoints";
interface GetDummyPodOptions { interface GetDummyPodOptions {
@ -12,16 +14,18 @@ interface GetDummyPodOptions {
initDead?: number; initDead?: number;
} }
function getDummyPodDefaultOptions(): Required<GetDummyPodOptions> { function getDummyPod(rawOpts: GetDummyPodOptions = {}): Pod {
return { const {
running: 0, running = 0,
dead: 0, dead = 0,
initDead: 0, initDead = 0,
initRunning: 0, initRunning = 0,
}; } = rawOpts;
}
function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Pod { const containers: PodContainer[] = [];
const initContainers: PodContainer[] = [];
const containerStatuses: PodContainerStatus[] = [];
const initContainerStatuses: PodContainerStatus[] = [];
const pod = new Pod({ const pod = new Pod({
apiVersion: "v1", apiVersion: "v1",
kind: "Pod", kind: "Pod",
@ -29,36 +33,35 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
uid: "1", uid: "1",
name: "test", name: "test",
resourceVersion: "v1", resourceVersion: "v1",
selfLink: "http", namespace: "default",
selfLink: "/api/v1/pods/default/test",
},
spec: {
containers,
initContainers,
serviceAccount: "dummy",
serviceAccountName: "dummy",
},
status: {
phase: "Running",
conditions: [],
hostIP: "10.0.0.1",
podIP: "10.0.0.1",
startTime: "now",
containerStatuses,
initContainerStatuses,
}, },
}); });
pod.spec = { for (let i = 0; i < running; i += 1) {
containers: [], const name = `container_running_${i}`;
initContainers: [],
serviceAccount: "dummy",
serviceAccountName: "dummy",
};
pod.status = { containers.push({
phase: "Running",
conditions: [],
hostIP: "10.0.0.1",
podIP: "10.0.0.1",
startTime: "now",
containerStatuses: [],
initContainerStatuses: [],
};
for (let i = 0; i < opts.running; i += 1) {
const name = `container_r_${i}`;
pod.spec.containers.push({
image: "dummy", image: "dummy",
imagePullPolicy: "dummy", imagePullPolicy: "dummy",
name, name,
}); });
pod.status.containerStatuses.push({ containerStatuses.push({
image: "dummy", image: "dummy",
imageID: "dummy", imageID: "dummy",
name, name,
@ -72,15 +75,15 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
}); });
} }
for (let i = 0; i < opts.dead; i += 1) { for (let i = 0; i < dead; i += 1) {
const name = `container_d_${i}`; const name = `container_dead_${i}`;
pod.spec.containers.push({ containers.push({
image: "dummy", image: "dummy",
imagePullPolicy: "dummy", imagePullPolicy: "dummy",
name, name,
}); });
pod.status.containerStatuses.push({ containerStatuses.push({
image: "dummy", image: "dummy",
imageID: "dummy", imageID: "dummy",
name, name,
@ -97,15 +100,15 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
}); });
} }
for (let i = 0; i < opts.initRunning; i += 1) { for (let i = 0; i < initRunning; i += 1) {
const name = `container_ir_${i}`; const name = `container_init-running_${i}`;
pod.spec.initContainers.push({ initContainers.push({
image: "dummy", image: "dummy",
imagePullPolicy: "dummy", imagePullPolicy: "dummy",
name, name,
}); });
pod.status.initContainerStatuses.push({ initContainerStatuses.push({
image: "dummy", image: "dummy",
imageID: "dummy", imageID: "dummy",
name, name,
@ -119,15 +122,15 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
}); });
} }
for (let i = 0; i < opts.initDead; i += 1) { for (let i = 0; i < initDead; i += 1) {
const name = `container_id_${i}`; const name = `container_init-dead_${i}`;
pod.spec.initContainers.push({ initContainers.push({
image: "dummy", image: "dummy",
imagePullPolicy: "dummy", imagePullPolicy: "dummy",
name, name,
}); });
pod.status.initContainerStatuses.push({ initContainerStatuses.push({
image: "dummy", image: "dummy",
imageID: "dummy", imageID: "dummy",
name, name,
@ -173,8 +176,8 @@ describe("Pods", () => {
it("getRunningContainers should return only running and init running", () => { it("getRunningContainers should return only running and init running", () => {
const res = [ const res = [
...Array.from(new Array(running), (val, index) => getNamedContainer(`container_r_${index}`)), ...Array.from(new Array(running), (val, index) => getNamedContainer(`container_running_${index}`)),
...Array.from(new Array(initRunning), (val, index) => getNamedContainer(`container_ir_${index}`)), ...Array.from(new Array(initRunning), (val, index) => getNamedContainer(`container_init-running_${index}`)),
]; ];
expect(pod.getRunningContainers()).toStrictEqual(res); expect(pod.getRunningContainers()).toStrictEqual(res);
@ -182,10 +185,10 @@ describe("Pods", () => {
it("getAllContainers should return all containers", () => { it("getAllContainers should return all containers", () => {
const res = [ const res = [
...Array.from(new Array(running), (val, index) => getNamedContainer(`container_r_${index}`)), ...Array.from(new Array(running), (val, index) => getNamedContainer(`container_running_${index}`)),
...Array.from(new Array(dead), (val, index) => getNamedContainer(`container_d_${index}`)), ...Array.from(new Array(dead), (val, index) => getNamedContainer(`container_dead_${index}`)),
...Array.from(new Array(initRunning), (val, index) => getNamedContainer(`container_ir_${index}`)), ...Array.from(new Array(initRunning), (val, index) => getNamedContainer(`container_init-running_${index}`)),
...Array.from(new Array(initDead), (val, index) => getNamedContainer(`container_id_${index}`)), ...Array.from(new Array(initDead), (val, index) => getNamedContainer(`container_init-dead_${index}`)),
]; ];
expect(pod.getAllContainers()).toStrictEqual(res); expect(pod.getAllContainers()).toStrictEqual(res);
@ -253,7 +256,7 @@ describe("Pods", () => {
it("should return true if a condition isn't ready", () => { it("should return true if a condition isn't ready", () => {
const pod = getDummyPod({ running: 1 }); const pod = getDummyPod({ running: 1 });
pod.status.conditions.push({ pod.status?.conditions.push({
type: "Ready", type: "Ready",
status: "foobar", status: "foobar",
lastProbeTime: 1, lastProbeTime: 1,
@ -266,7 +269,7 @@ describe("Pods", () => {
it("should return false if a condition is non-ready", () => { it("should return false if a condition is non-ready", () => {
const pod = getDummyPod({ running: 1 }); const pod = getDummyPod({ running: 1 });
pod.status.conditions.push({ pod.status?.conditions.push({
type: "dummy", type: "dummy",
status: "foobar", status: "foobar",
lastProbeTime: 1, lastProbeTime: 1,
@ -278,8 +281,11 @@ describe("Pods", () => {
it("should return true if a current container is in a crash loop back off", () => { it("should return true if a current container is in a crash loop back off", () => {
const pod = getDummyPod({ running: 1 }); const pod = getDummyPod({ running: 1 });
const firstStatus = pod.status?.containerStatuses?.[0];
pod.status.containerStatuses[0].state = { assert(firstStatus);
firstStatus.state = {
waiting: { waiting: {
reason: "CrashLookBackOff", reason: "CrashLookBackOff",
message: "too much foobar", message: "too much foobar",
@ -292,6 +298,8 @@ describe("Pods", () => {
it("should return true if a current phase isn't running", () => { it("should return true if a current phase isn't running", () => {
const pod = getDummyPod({ running: 1 }); const pod = getDummyPod({ running: 1 });
assert(pod.status);
pod.status.phase = "not running"; pod.status.phase = "not running";
expect(pod.hasIssues()).toStrictEqual(true); expect(pod.hasIssues()).toStrictEqual(true);
@ -300,6 +308,8 @@ describe("Pods", () => {
it("should return false if a current phase is running", () => { it("should return false if a current phase is running", () => {
const pod = getDummyPod({ running: 1 }); const pod = getDummyPod({ running: 1 });
assert(pod.status);
pod.status.phase = "Running"; pod.status.phase = "Running";
expect(pod.hasIssues()).toStrictEqual(false); expect(pod.hasIssues()).toStrictEqual(false);

View File

@ -3,31 +3,39 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { StatefulSet, StatefulSetApi } from "../endpoints/stateful-set.api"; import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
import apiKubeInjectable from "../../../renderer/k8s/api-kube.injectable";
import type { StatefulSetApi } from "../endpoints";
import statefulSetApiInjectable from "../endpoints/stateful-set.api.injectable";
import type { KubeJsonApi } from "../kube-json-api"; import type { KubeJsonApi } from "../kube-json-api";
class StatefulSetApiTest extends StatefulSetApi {
public setRequest(request: any) {
this.request = request;
}
}
describe("StatefulSetApi", () => { describe("StatefulSetApi", () => {
let statefulSetApi: StatefulSetApi;
let kubeJsonApi: jest.Mocked<KubeJsonApi>;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(storesAndApisCanBeCreatedInjectable, () => true);
kubeJsonApi = {
getResponse: jest.fn(),
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
patch: jest.fn(),
del: jest.fn(),
} as never;
di.override(apiKubeInjectable, () => kubeJsonApi);
statefulSetApi = di.inject(statefulSetApiInjectable);
});
describe("scale", () => { describe("scale", () => {
const requestMock = {
patch: () => ({}),
} as unknown as KubeJsonApi;
const sub = new StatefulSetApiTest({ objectConstructor: StatefulSet });
sub.setRequest(requestMock);
it("requests Kubernetes API with PATCH verb and correct amount of replicas", () => { it("requests Kubernetes API with PATCH verb and correct amount of replicas", () => {
const patchSpy = jest.spyOn(requestMock, "patch"); statefulSetApi.scale({ namespace: "default", name: "statefulset-1" }, 5);
sub.scale({ namespace: "default", name: "statefulset-1" }, 5); expect(kubeJsonApi.patch).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/statefulsets/statefulset-1/scale", {
expect(patchSpy).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/statefulsets/statefulset-1/scale", {
data: { data: {
spec: { spec: {
replicas: 5, replicas: 5,

View File

@ -3,18 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { isClusterPageContext } from "../utils"; import { getInjectionToken } from "@ogre-tools/injectable";
import { KubeJsonApi } from "./kube-json-api"; import { asLegacyGlobalForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import { apiKubePrefix, isDevelopment } from "../vars"; import type { KubeJsonApi } from "./kube-json-api";
export const apiKube = isClusterPageContext() export const apiKubeInjectionToken = getInjectionToken<KubeJsonApi>({
? new KubeJsonApi({ id: "api-kube-injection-token",
serverAddress: `http://127.0.0.1:${window.location.port}`, });
apiBase: apiKubePrefix,
debug: isDevelopment, export const apiKube = asLegacyGlobalForExtensionApi(apiKubeInjectionToken);
}, {
headers: {
"Host": window.location.host,
},
})
: undefined;

View File

@ -1,123 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeObjectStore } from "./kube-object.store";
import { action, observable, makeObservable } from "mobx";
import { autoBind, iter } from "../utils";
import type { KubeApi } from "./kube-api";
import type { KubeObject } from "./kube-object";
import type { IKubeObjectRef } from "./kube-api-parse";
import { parseKubeApi, createKubeApiURL } from "./kube-api-parse";
export class ApiManager {
private apis = observable.map<string, KubeApi<KubeObject>>();
private stores = observable.map<string, KubeObjectStore<KubeObject>>();
constructor() {
makeObservable(this);
autoBind(this);
}
getApi(pathOrCallback: string | ((api: KubeApi<KubeObject>) => boolean)) {
if (typeof pathOrCallback === "string") {
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase);
}
return iter.find(this.apis.values(), pathOrCallback ?? (() => true));
}
getApiByKind(kind: string, apiVersion: string) {
return iter.find(this.apis.values(), api => api.kind === kind && api.apiVersionWithGroup === apiVersion);
}
registerApi<K extends KubeObject>(apiBase: string, api: KubeApi<K>) {
if (!api.apiBase) return;
if (!this.apis.has(apiBase)) {
this.stores.forEach((store) => {
if (store.api === api) {
this.stores.set(apiBase, store);
}
});
this.apis.set(apiBase, api);
}
}
protected resolveApi(api?: string | KubeApi<KubeObject>): KubeApi<KubeObject> | undefined {
if (!api) {
return undefined;
}
if (typeof api === "string") {
return this.getApi(api) as KubeApi<KubeObject>;
}
return api;
}
unregisterApi(api: string | KubeApi<KubeObject>) {
if (typeof api === "string") this.apis.delete(api);
else {
const apis = Array.from(this.apis.entries());
const entry = apis.find(entry => entry[1] === api);
if (entry) this.unregisterApi(entry[0]);
}
}
@action
registerStore<K extends KubeObject>(store: KubeObjectStore<K>, apis: KubeApi<K>[] = [store.api]) {
apis.filter(Boolean).forEach(api => {
if (api.apiBase) this.stores.set(api.apiBase, store);
});
}
getStore<S extends KubeObjectStore<KubeObject>>(api: string | KubeApi<KubeObject>): S | undefined {
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
}
lookupApiLink(ref: IKubeObjectRef, parentObject?: KubeObject): string {
const {
kind, apiVersion, name,
namespace = parentObject?.getNs(),
} = ref;
if (!kind) return "";
// search in registered apis by 'kind' & 'apiVersion'
const api = this.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion);
if (api) {
return api.getUrl({ namespace, name });
}
// lookup api by generated resource link
const apiPrefixes = ["/apis", "/api"];
const resource = kind.endsWith("s") ? `${kind.toLowerCase()}es` : `${kind.toLowerCase()}s`;
for (const apiPrefix of apiPrefixes) {
const apiLink = createKubeApiURL({ apiPrefix, apiVersion, name, namespace, resource });
if (this.getApi(apiLink)) {
return apiLink;
}
}
// resolve by kind only (hpa's might use refs to older versions of resources for example)
const apiByKind = this.getApi(api => api.kind === kind);
if (apiByKind) {
return apiByKind.getUrl({ name, namespace });
}
// otherwise generate link with default prefix
// resource still might exists in k8s, but api is not registered in the app
return createKubeApiURL({ apiVersion, name, namespace, resource });
}
}
export const apiManager = new ApiManager();

Some files were not shown because too many files have changed in this diff Show More