Merge branch 'master' into fix-load-release-data-on-details-open

This commit is contained in:
Alex Andreev authored and GitHub committed 2023-02-22 09:38:03 +03:00
commit aa2fcd2c6b
64 files changed
+34126 -36975

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
disturl "https://electronjs.org/headers"
target "19.0.4"
runtime "electron"
+10 -10
View File
@@ -50,14 +50,14 @@
"license": "MIT",
"author": "OpenLens Authors <info@k8slens.dev>",
"scripts": {
"build": "cross-env NODE_ENV=production yarn run webpack --config webpack/library-bundle.ts --progress",
"build": "cross-env NODE_ENV=production webpack --config webpack/library-bundle.ts --progress",
"clean": "rimraf dist static/build",
"prepare:dev": "cross-env NODE_ENV=development yarn run webpack --config webpack/library-bundle.ts --progress",
"dev": "cross-env NODE_ENV=development yarn run webpack --config webpack/library-bundle.ts --watch",
"prepare:dev": "cross-env NODE_ENV=development webpack --config webpack/library-bundle.ts --progress",
"dev": "cross-env NODE_ENV=development webpack --config webpack/library-bundle.ts --watch",
"test:unit": "jest --testPathIgnorePatterns integration",
"test:watch": "func() { jest ${1} --watch --testPathIgnorePatterns integration; }; func",
"lint": "PROD=true yarn run eslint --ext js,ts,tsx --max-warnings=0 .",
"lint:fix": "yarn run lint --fix"
"lint": "PROD=true eslint --ext js,ts,tsx --max-warnings=0 .",
"lint:fix": "npm run lint --fix"
},
"config": {
"k8sProxyVersion": "0.3.0",
@@ -192,7 +192,7 @@
"win-ca": "^3.5.0",
"winston": "^3.8.2",
"winston-transport-browserconsole": "^1.0.5",
"ws": "^8.12.0",
"ws": "^8.12.1",
"xterm-link-provider": "^1.3.1"
},
"devDependencies": {
@@ -242,7 +242,7 @@
"@types/react-window": "^1.8.5",
"@types/readable-stream": "^2.3.13",
"@types/semver": "^7.3.13",
"@types/tar": "^6.1.3",
"@types/tar": "^6.1.4",
"@types/tcp-port-used": "^1.0.1",
"@types/tempy": "^0.3.0",
"@types/triple-beam": "^1.3.2",
@@ -310,7 +310,7 @@
"react-table": "^7.8.0",
"react-window": "^1.8.8",
"rimraf": "^4.1.2",
"sass": "^1.58.1",
"sass": "^1.58.2",
"sass-loader": "^12.6.0",
"style-loader": "^3.3.1",
"tailwindcss": "^3.2.4",
@@ -318,7 +318,7 @@
"ts-node": "^10.9.1",
"type-fest": "^2.14.0",
"typed-emitter": "^1.4.0",
"typedoc": "0.23.24",
"typedoc": "0.23.25",
"typedoc-plugin-markdown": "^3.13.6",
"typescript": "^4.9.5",
"typescript-plugin-css-modules": "^3.4.0",
@@ -340,7 +340,7 @@
"@types/react-router-dom": "^5.3.3",
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"@types/tar": "^6.1.3",
"@types/tar": "^6.1.4",
"@types/tcp-port-used": "^1.0.1",
"@types/url-parse": "^1.4.8",
"@types/uuid": "^8.3.4",
@@ -37,18 +37,17 @@ export interface RequestMetricsParams {
namespace?: string;
}
export interface RequestMetrics {
(query: string, params?: RequestMetricsParams): Promise<MetricData>;
(query: string[], params?: RequestMetricsParams): Promise<MetricData[]>;
<Keys extends string>(query: Record<Keys, Partial<Record<string, string>>>, params?: RequestMetricsParams): Promise<Record<Keys, MetricData>>;
}
export type RequestMetrics = ReturnType<typeof requestMetricsInjectable["instantiate"]>;
const requestMetricsInjectable = getInjectable({
id: "request-metrics",
instantiate: (di) => {
const apiBase = di.inject(apiBaseInjectable);
return (async (query: object, params: RequestMetricsParams = {}) => {
function requestMetrics(query: string, params?: RequestMetricsParams): Promise<MetricData>;
function requestMetrics(query: string[], params?: RequestMetricsParams): Promise<MetricData[]>;
function requestMetrics<Keys extends string>(query: Record<Keys, Partial<Record<string, string>>>, params?: RequestMetricsParams): Promise<Record<Keys, MetricData>>;
async function requestMetrics(query: string | string[] | Partial<Record<string, Partial<Record<string, string>>>>, params: RequestMetricsParams = {}): Promise<MetricData | MetricData[] | Partial<Record<string, MetricData>>> {
const { range = 3600, step = 60, namespace } = params;
let { start, end } = params;
@@ -66,7 +65,9 @@ const requestMetricsInjectable = getInjectable({
"kubernetes_namespace": namespace,
},
});
}) as RequestMetrics;
}
return requestMetrics;
},
});
@@ -6,7 +6,7 @@
import { getGlobalOverride } from "../test-utils/get-global-override";
import randomBytesInjectable from "./random-bytes.injectable";
export default getGlobalOverride(randomBytesInjectable, () => async (size) => {
export default getGlobalOverride(randomBytesInjectable, () => (size) => {
const res = Buffer.alloc(size);
for (let i = 0; i < size; i += 1) {
@@ -4,13 +4,12 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { randomBytes } from "crypto";
import { promisify } from "util";
export type RandomBytes = (size: number) => Promise<Buffer>;
export type RandomBytes = (size: number) => Buffer;
const randomBytesInjectable = getInjectable({
id: "random-bytes",
instantiate: (): RandomBytes => promisify(randomBytes),
instantiate: (): RandomBytes => randomBytes,
causesSideEffects: true,
});
@@ -3,17 +3,18 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ObservableMap } from "mobx";
import { getInjectable } from "@ogre-tools/injectable";
import { getOrInsertWithAsync } from "../../../common/utils";
import { getOrInsert } from "../../../common/utils";
import randomBytesInjectable from "../../../common/utils/random-bytes.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable";
import ensureDirInjectable from "../../../common/fs/ensure-dir.injectable";
import getHashInjectable from "./get-hash.injectable";
import getPathToLegacyPackageJsonInjectable from "./get-path-to-legacy-package-json.injectable";
import { registeredExtensionsInjectable } from "./registered-extensions.injectable";
export type EnsureHashedDirectoryForExtension = (extensionName: string, registeredExtensions: ObservableMap<string, string>) => Promise<string>;
export type EnsureHashedDirectoryForExtension = (extensionName: string) => Promise<string>;
const ensureHashedDirectoryForExtensionInjectable = getInjectable({
id: "ensure-hashed-directory-for-extension",
@@ -24,14 +25,27 @@ const ensureHashedDirectoryForExtensionInjectable = getInjectable({
const directoryForExtensionData = di.inject(directoryForExtensionDataInjectable);
const ensureDirectory = di.inject(ensureDirInjectable);
const getHash = di.inject(getHashInjectable);
const getPathToLegacyPackageJson = di.inject(getPathToLegacyPackageJsonInjectable);
const registeredExtensions = di.inject(registeredExtensionsInjectable);
return async (extensionName, registeredExtensions) => {
const dirPath = await getOrInsertWithAsync(registeredExtensions, extensionName, async () => {
const salt = (await randomBytes(32)).toString("hex");
return async (extensionName) => {
let dirPath: string;
const legacyDirPath = getPathToLegacyPackageJson(extensionName);
const hashedDirectoryForLegacyDirPath = registeredExtensions.get(legacyDirPath);
if (hashedDirectoryForLegacyDirPath) {
registeredExtensions.set(extensionName, hashedDirectoryForLegacyDirPath);
registeredExtensions.delete(legacyDirPath);
dirPath = hashedDirectoryForLegacyDirPath;
} else {
const salt = randomBytes(32).toString("hex");
const hashedName = getHash(`${extensionName}/${salt}`);
return joinPaths(directoryForExtensionData, hashedName);
});
const hashedExtensionDirectory = joinPaths(directoryForExtensionData, hashedName);
dirPath = getOrInsert(registeredExtensions, extensionName, hashedExtensionDirectory);
}
await ensureDirectory(dirPath);
@@ -0,0 +1,96 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ObservableMap } from "mobx";
import { runInAction } from "mobx";
import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting";
import type { EnsureHashedDirectoryForExtension } from "./ensure-hashed-directory-for-extension.injectable";
import ensureHashedDirectoryForExtensionInjectable from "./ensure-hashed-directory-for-extension.injectable";
import ensureDirInjectable from "../../../common/fs/ensure-dir.injectable";
import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { registeredExtensionsInjectable } from "./registered-extensions.injectable";
describe("ensure-hashed-directory-for-extension", () => {
let ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension;
let ensureDirMock: jest.Mock;
let registeredExtensions: ObservableMap<string, string>;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
ensureDirMock = jest.fn();
di.override(ensureDirInjectable, () => ensureDirMock);
di.override(directoryForExtensionDataInjectable, () => "some-directory-for-extension-data");
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
ensureHashedDirectoryForExtension = di.inject(
ensureHashedDirectoryForExtensionInjectable,
);
registeredExtensions = di.inject(registeredExtensionsInjectable);
});
it("given registered extension exists, returns existing directory", async () => {
runInAction(() => {
registeredExtensions.set("some-extension-name", "some-directory");
});
const actual = await ensureHashedDirectoryForExtension(
"some-extension-name",
);
expect(actual).toBe("some-directory");
});
it("given registered extension does not exist, returns random directory", async () => {
const actual = await ensureHashedDirectoryForExtension(
"some-extension-name",
);
expect(actual).toBe("some-directory-for-extension-data/a37a1cfefc0391af3733f23cb6b29443f596a2b8ffe6d116c35df7bc3cd99ef6");
});
describe("given extension directory was saved based on extension's package.json path", () => {
beforeEach(() => {
runInAction(() => {
registeredExtensions.set("/some-directory-for-user-data/node_modules/some-extension-name/package.json", "some-directory");
});
ensureDirMock.mockClear();
});
it("returns existing directory", async () => {
const actual = await ensureHashedDirectoryForExtension(
"some-extension-name",
);
expect(actual).toBe("some-directory");
});
it("ensure dir is called with some directory", async () => {
await ensureHashedDirectoryForExtension(
"some-extension-name",
);
expect(ensureDirMock).toHaveBeenCalledWith("some-directory");
});
it("is migrated to use the extension name as key", async () => {
await ensureHashedDirectoryForExtension(
"some-extension-name",
);
expect(registeredExtensions.get("some-extension-name")).toEqual("some-directory");
});
it("old key is removed", async () => {
await ensureHashedDirectoryForExtension(
"some-extension-name",
);
expect(registeredExtensions.has("/some-directory-for-user-data/node_modules/some-extension-name/package.json")).toEqual(false);
});
});
});
@@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { MigrationDeclaration } from "../../../common/base-store/migrations.injectable";
export const fileSystemProvisionerStoreInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "file-system-provisioner-store-injection-token",
});
@@ -14,6 +14,7 @@ import { persistStateToConfigInjectionToken } from "../../../common/base-store/s
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../../../common/utils/channel/enlist-message-channel-listener-injection-token";
import ensureHashedDirectoryForExtensionInjectable from "./ensure-hashed-directory-for-extension.injectable";
import { registeredExtensionsInjectable } from "./registered-extensions.injectable";
const fileSystemProvisionerStoreInjectable = getInjectable({
id: "file-system-provisioner-store",
@@ -30,6 +31,7 @@ const fileSystemProvisionerStoreInjectable = getInjectable({
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
ensureHashedDirectoryForExtension: di.inject(ensureHashedDirectoryForExtensionInjectable),
registeredExtensions: di.inject(registeredExtensionsInjectable),
}),
});
@@ -3,7 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, makeObservable, observable } from "mobx";
import type { ObservableMap } from "mobx";
import { action, makeObservable } from "mobx";
import type { BaseStoreDependencies } from "../../../common/base-store/base-store";
import { BaseStore } from "../../../common/base-store/base-store";
import type { LensExtensionId } from "../../lens-extension";
@@ -16,11 +17,10 @@ interface FSProvisionModel {
interface Dependencies extends BaseStoreDependencies {
ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension;
registeredExtensions: ObservableMap<LensExtensionId, string>;
}
export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
readonly registeredExtensions = observable.map<LensExtensionId, string>();
constructor(protected readonly dependencies: Dependencies) {
super(dependencies, {
configName: "lens-filesystem-provisioner-store",
@@ -37,17 +37,17 @@ export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
* @returns path to the folder that the extension can safely write files to.
*/
async requestDirectory(extensionName: string): Promise<string> {
return this.dependencies.ensureHashedDirectoryForExtension(extensionName, this.registeredExtensions);
return this.dependencies.ensureHashedDirectoryForExtension(extensionName);
}
@action
protected fromStore({ extensions }: FSProvisionModel = { extensions: {}}): void {
this.registeredExtensions.merge(extensions);
this.dependencies.registeredExtensions.merge(extensions);
}
toJSON(): FSProvisionModel {
return toJS({
extensions: Object.fromEntries(this.registeredExtensions),
extensions: Object.fromEntries(this.dependencies.registeredExtensions),
});
}
}
@@ -0,0 +1,22 @@
/**
* 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 directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
export type GetPathToLegacyPackageJson = (extensionName: string) => string;
const getPathToLegacyPackageJson = getInjectable({
id: "get-path-to-legacy-package-json",
instantiate: (di): GetPathToLegacyPackageJson => {
const directoryForUserData = di.inject(directoryForUserDataInjectable);
const joinPaths = di.inject(joinPathsInjectable);
return (extensionName: string) => joinPaths(directoryForUserData, "node_modules", extensionName, "package.json");
},
});
export default getPathToLegacyPackageJson;
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { observable } from "mobx";
import type { LensExtensionId } from "../../lens-extension";
export const registeredExtensionsInjectable = getInjectable({
id: "registered-extensions",
instantiate: () => observable.map<LensExtensionId, string>(),
});
@@ -38,6 +38,8 @@ import kubeEventApiInjectable from "../../common/k8s-api/endpoints/events.api.in
import roleBindingApiInjectable from "../../common/k8s-api/endpoints/role-binding.api.injectable";
import customResourceDefinitionApiInjectable from "../../common/k8s-api/endpoints/custom-resource-definition.api.injectable";
import { shouldShowResourceInjectionToken } from "../../common/cluster-store/allowed-resources-injection-token";
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
import requestMetricsInjectable from "../../common/k8s-api/endpoints/metrics.api/request-metrics.injectable";
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
const di = getLegacyGlobalDiForExtensionApi();
@@ -93,6 +95,17 @@ export const crdApi = asLegacyGlobalForExtensionApi(customResourceDefinitionApiI
export * from "../common-api/k8s-api";
export const requestMetrics = asLegacyGlobalFunctionForExtensionApi(requestMetricsInjectable);
export type {
RequestMetrics,
RequestMetricsParams,
} from "../../common/k8s-api/endpoints/metrics.api/request-metrics.injectable";
export type {
MetricData,
MetricResult,
} from "../../common/k8s-api/endpoints/metrics.api";
export {
KubeObjectStatusLevel,
type KubeObjectStatus,
@@ -4507,3 +4507,497 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div>
</div>
`;
exports[`cluster - sidebar and tab navigation for extensions given extension with cluster pages and cluster page menus with explicit 'orderNumber' given no state for expanded sidebar items exists, and navigated to child sidebar item, when rendered renders 1`] = `
<div>
<div
class="Animate slide-right Drawer KubeObjectDetails flex column right enter leave"
style="--size: 725px; --enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="drawer-wrapper flex column"
>
<div
class="drawer-title flex align-center"
>
<div
class="drawer-title-text flex gaps align-center"
>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
<div>
Close
</div>
</div>
<div
class="drawer-content flex column box grow"
/>
</div>
<div
class="ResizingAnchor horizontal leading"
/>
</div>
<div
class="Notifications flex column align-flex-end"
/>
<div
class="mainLayout"
style="--sidebar-width: 200px;"
>
<div
class="sidebar"
>
<div
class="flex flex-col"
data-testid="cluster-sidebar"
>
<div
class="SidebarCluster"
>
<div
class="Avatar rounded loadingAvatar"
style="width: 40px; height: 40px;"
>
??
</div>
<div
class="loadingClusterName"
/>
</div>
<div
class="sidebarNav sidebar-active-status"
>
<div
class="SidebarItem"
data-is-active-test="true"
data-testid="sidebar-item-some-extension-name-some-parent-id"
>
<a
aria-current="page"
class="navItem active"
data-testid="sidebar-item-link-for-some-extension-name-some-parent-id"
href="/"
>
<div>
Some icon
</div>
<span>
Parent
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-workloads"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-workloads"
href="/"
>
<i
class="Icon svg focusable"
>
<span
class="icon"
/>
</i>
<span>
Workloads
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-config"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-config"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="list"
>
list
</span>
</i>
<span>
Config
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-network"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-network"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="device_hub"
>
device_hub
</span>
</i>
<span>
Network
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-storage"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-storage"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="storage"
>
storage
</span>
</i>
<span>
Storage
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-helm"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-helm"
href="/"
>
<i
class="Icon svg focusable"
>
<span
class="icon"
/>
</i>
<span>
Helm
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-user-management"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-user-management"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="security"
>
security
</span>
</i>
<span>
Access Control
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-custom-resources"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-custom-resources"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="extension"
>
extension
</span>
</i>
<span>
Custom Resources
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
</div>
</div>
<div
class="ResizingAnchor horizontal trailing"
/>
</div>
<div
class="contents"
>
<div
class="TabLayout"
data-testid="tab-layout"
>
<div
class="Tabs center scrollable"
>
<div
class="Tab flex gaps align-center active"
data-is-active-test="true"
data-testid="tab-link-for-some-extension-name-some-child-id"
role="tab"
tabindex="0"
>
<div
class="label"
>
Child 1
</div>
</div>
<div
class="Tab flex gaps align-center"
data-is-active-test="false"
data-testid="tab-link-for-some-extension-name-some-other-child-id"
role="tab"
tabindex="0"
>
<div
class="label"
>
Child 2
</div>
</div>
</div>
<main
class=""
>
<div
data-testid="some-child-page"
>
Some child page
</div>
</main>
</div>
</div>
<div
class="footer"
>
<div
class="Dock"
tabindex="-1"
>
<div
class="ResizingAnchor vertical leading"
/>
<div
class="tabs-container flex align-center"
>
<div
class="dockTabs"
role="tablist"
>
<div
class="Tabs tabs"
>
<div
class="Tab flex gaps align-center DockTab TerminalTab active"
data-testid="dock-tab-for-terminal"
id="tab-terminal"
role="tab"
tabindex="0"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="terminal"
>
terminal
</span>
</i>
<div
class="label"
>
<div
class="flex align-center"
>
<span
class="title"
>
Terminal
</span>
<div
class="close"
>
<i
class="Icon material interactive focusable small"
data-testid="dock-tab-close-for-terminal"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
<div
data-testid="tooltip-content-for-dock-tab-close-for-terminal"
>
Close ⌘+W
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="toolbar flex gaps align-center box grow"
>
<div
class="dock-menu box grow"
>
<i
class="Icon new-dock-tab material interactive focusable"
id="menu-actions-for-dock"
tabindex="0"
>
<span
class="icon"
data-icon-name="add"
>
add
</span>
</i>
<div>
New tab
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="fullscreen"
>
fullscreen
</span>
</i>
<div>
Fit to window
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="keyboard_arrow_up"
>
keyboard_arrow_up
</span>
</i>
<div>
Open
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@@ -79,7 +79,11 @@ exports[`disable kube object detail items when cluster is not relevant given ext
<span
class="value"
>
some-namespace
<a
href="/workloads?kube-details=%2Fapi%2Fv1%2Fnamespaces%2Fsome-namespace&kube-selected=%2Fapis%2Fsome-api-version%2Fnamespaces%2Fsome-namespace%2Fsome-kind%2Fsome-name"
>
some-namespace
</a>
</span>
</div>
<div
@@ -695,7 +699,11 @@ exports[`disable kube object detail items when cluster is not relevant given ext
<span
class="value"
>
some-namespace
<a
href="/workloads?kube-details=%2Fapi%2Fv1%2Fnamespaces%2Fsome-namespace&kube-selected=%2Fapis%2Fsome-api-version%2Fnamespaces%2Fsome-namespace%2Fsome-kind%2Fsome-name"
>
some-namespace
</a>
</span>
</div>
<div>
@@ -1306,7 +1314,11 @@ exports[`disable kube object detail items when cluster is not relevant given not
<span
class="value"
>
some-namespace
<a
href="/workloads?kube-details=%2Fapi%2Fv1%2Fnamespaces%2Fsome-namespace&kube-selected=%2Fapis%2Fsome-api-version%2Fnamespaces%2Fsome-namespace%2Fsome-kind%2Fsome-name"
>
some-namespace
</a>
</span>
</div>
<div>
@@ -79,7 +79,11 @@ exports[`reactively hide kube object detail item renders 1`] = `
<span
class="value"
>
some-namespace
<a
href="/workloads?kube-details=%2Fapi%2Fv1%2Fnamespaces%2Fsome-namespace&kube-selected=%2Fapis%2Fsome-api-version%2Fnamespaces%2Fsome-namespace%2Fsome-kind%2Fsome-name"
>
some-namespace
</a>
</span>
</div>
<div>
@@ -690,7 +694,11 @@ exports[`reactively hide kube object detail item when the item is shown renders
<span
class="value"
>
some-namespace
<a
href="/workloads?kube-details=%2Fapi%2Fv1%2Fnamespaces%2Fsome-namespace&kube-selected=%2Fapis%2Fsome-api-version%2Fnamespaces%2Fsome-namespace%2Fsome-kind%2Fsome-name"
>
some-namespace
</a>
</span>
</div>
<div
@@ -21,6 +21,7 @@ import { runInAction, computed, observable } from "mobx";
import storageSaveDelayInjectable from "../../renderer/utils/create-storage/storage-save-delay.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import { flushPromises } from "../../common/test-utils/flush-promises";
import type { ClusterPageMenuRegistration } from "../../extensions/common-api/types";
describe("cluster - sidebar and tab navigation for extensions", () => {
let applicationBuilder: ApplicationBuilder;
@@ -139,15 +140,12 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
const route = windowDi
.inject(routesInjectable)
.get()
.find(
matches({
path: "/extension/some-extension-name/some-child-page-id",
}),
);
.find(matches({
path: "/extension/some-extension-name/some-child-page-id",
}));
assert(route);
navigateToRoute(route);
});
it("renders", () => {
@@ -453,4 +451,131 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
});
});
});
describe("given extension with cluster pages and cluster page menus with explicit 'orderNumber'", () => {
let someObservable: IObservableValue<boolean>;
beforeEach(() => {
someObservable = observable.box(false);
const testExtension = {
id: "some-extension-id",
name: "some-extension-name",
rendererOptions: {
clusterPages: [
{
components: {
Page: () => {
throw new Error("should never come here");
},
},
},
{
id: "some-child-page-id",
components: {
Page: () => <div data-testid="some-child-page">Some child page</div>,
},
},
{
id: "some-other-child-page-id",
components: {
Page: () => (
<div data-testid="some-other-child-page">Some other child page</div>
),
},
},
],
clusterPageMenus: [
{
id: "some-parent-id",
title: "Parent",
components: {
Icon: () => <div>Some icon</div>,
},
orderNumber: -Infinity,
},
{
id: "some-child-id",
target: { pageId: "some-child-page-id" },
parentId: "some-parent-id",
title: "Child 1",
components: {
Icon: null as never,
},
},
{
id: "some-other-child-id",
target: { pageId: "some-other-child-page-id" },
parentId: "some-parent-id",
title: "Child 2",
components: {
Icon: null as never,
},
},
{
id: "some-menu-with-controlled-visibility",
title: "Some menu with controlled visibility",
visible: computed(() => someObservable.get()),
components: {
Icon: () => <div>Some icon</div>,
},
},
] as ClusterPageMenuRegistration[],
},
};
applicationBuilder.extensions.enable(testExtension);
});
describe("given no state for expanded sidebar items exists, and navigated to child sidebar item, when rendered", () => {
beforeEach(async () => {
rendered = await applicationBuilder.render();
const windowDi = applicationBuilder.applicationWindow.only.di;
const navigateToRoute = windowDi.inject(navigateToRouteInjectionToken);
const route = windowDi
.inject(routesInjectable)
.get()
.find(matches({
path: "/extension/some-extension-name/some-child-page-id",
}));
assert(route);
navigateToRoute(route);
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("renderes parent as first item in sidebar", () => {
const parent = rendered.getByTestId("sidebar-item-some-extension-name-some-parent-id");
assert(parent);
expect(parent.previousSibling).toBeNull();
});
it("parent is highlighted", () => {
const parent = rendered.getByTestId("sidebar-item-some-extension-name-some-parent-id");
expect(parent?.dataset.isActiveTest).toBe("true");
});
it("parent sidebar item is not expanded", () => {
const child = rendered.queryByTestId("sidebar-item-some-extension-name-some-child-id");
expect(child).toBeNull();
});
it("child page is shown", () => {
expect(rendered.getByTestId("some-child-page")).not.toBeNull();
});
});
});
});
+3 -11
View File
@@ -29,17 +29,9 @@ export async function bootstrap(di: DiContainer) {
assert(rootElem, "#app MUST exist");
const extensionLoader = di.inject(extensionLoaderInjectable);
extensionLoader.init();
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
extensionDiscovery.init();
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
extensionInstallationStateStore.bindIpcListeners();
await di.inject(extensionLoaderInjectable).init();
await di.inject(extensionDiscoveryInjectable).init();
di.inject(extensionInstallationStateStoreInjectable).bindIpcListeners();
let App;
let initializeApp;
@@ -9,7 +9,7 @@ import React from "react";
import upperFirst from "lodash/upperFirst";
import kebabCase from "lodash/kebabCase";
import { disposeOnUnmount, observer } from "mobx-react";
import { DrawerItem, DrawerItemLabels } from "../drawer";
import { DrawerItem } from "../drawer";
import { Badge } from "../badge";
import type { KubeObjectDetailsProps } from "../kube-object-details";
import { formatNodeTaint, Node } from "../../../common/k8s-api/endpoints";
@@ -23,7 +23,8 @@ import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.inj
import type { PodStore } from "../+workloads-pods/store";
import podStoreInjectable from "../+workloads-pods/store.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import loadPodsFromAllNamespacesInjectable from "../+workloads-pods/load-pods-from-all-namespaces.injectable";
import loadPodsFromAllNamespacesInjectable
from "../+workloads-pods/load-pods-from-all-namespaces.injectable";
export interface NodeDetailsProps extends KubeObjectDetailsProps<Node> {
}
@@ -97,14 +98,6 @@ class NonInjectedNodeDetails extends React.Component<NodeDetailsProps & Dependen
</DrawerItem>
</>
)}
<DrawerItemLabels
name="Labels"
labels={node.getLabels()}
/>
<DrawerItemLabels
name="Annotations"
labels={node.getAnnotations()}
/>
{taints.length > 0 && (
<DrawerItem name="Taints" labelsOnly>
{taints.map(taint => <Badge key={taint.key} label={formatNodeTaint(taint)} />)}
@@ -7,10 +7,6 @@
--overlay-bg: #8cc474b8;
--overlay-active-bg: orange;
// fix for `this.logsElement.scrollTop = this.logsElement.scrollHeight`
// `overflow: overlay` don't allow scroll to the last line
overflow: auto;
position: relative;
color: var(--logsForeground);
background: var(--logsBackground);
@@ -21,6 +17,7 @@
.list {
overflow-x: scroll!important;
overflow-y: auto!important;
.LogRow {
padding: 2px 16px;
@@ -28,7 +28,7 @@ export interface InstantiateArgs {
const logsViewModelInjectable = getInjectable({
id: "logs-view-model",
instantiate: (di, { tabId }: InstantiateArgs) => new LogTabViewModel(tabId, {
instantiate: (di, tabId) => new LogTabViewModel(tabId, {
getLogs: di.inject(getLogsInjectable),
getLogsWithoutTimestamps: di.inject(getLogsWithoutTimestampsInjectable),
getTimestampSplitLogs: di.inject(getTimestampSplitLogsInjectable),
@@ -45,7 +45,10 @@ const logsViewModelInjectable = getInjectable({
downloadAllLogs: di.inject(downloadAllLogsInjectable),
searchStore: di.inject(searchStoreInjectable),
}),
lifecycle: lifecycleEnum.transient,
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, tabId: TabId) => `log-tab-view-model-${tabId}}`,
}),
});
export default logsViewModelInjectable;
@@ -101,9 +101,7 @@ const NonInjectedLogsDockTab = observer(({
export const LogsDockTab = withInjectables<Dependencies, LogsDockTabProps>(NonInjectedLogsDockTab, {
getProps: (di, props) => ({
...props,
model: di.inject(logsViewModelInjectable, {
tabId: props.tab.id,
}),
model: di.inject(logsViewModelInjectable, props.tab.id),
subscribeStores: di.inject(subscribeStoresInjectable),
podStore: di.inject(podStoreInjectable),
}),
@@ -19,6 +19,8 @@ import { withInjectables } from "@ogre-tools/injectable-react";
import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable";
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import type { NamespaceApi } from "../../../common/k8s-api/endpoints";
import namespaceApiInjectable from "../../../common/k8s-api/endpoints/namespace.api.injectable";
export interface KubeObjectMetaProps {
object: KubeObject;
@@ -28,20 +30,24 @@ export interface KubeObjectMetaProps {
interface Dependencies {
getDetailsUrl: GetDetailsUrl;
apiManager: ApiManager;
namespaceApi: NamespaceApi;
logger: Logger;
}
const NonInjectedKubeObjectMeta = observer(({
apiManager,
getDetailsUrl,
object,
hideFields = [
"uid",
"resourceVersion",
"selfLink",
],
logger,
}: Dependencies & KubeObjectMetaProps) => {
const NonInjectedKubeObjectMeta = observer((
{
apiManager,
getDetailsUrl,
object,
hideFields = [
"uid",
"resourceVersion",
"selfLink",
],
logger,
namespaceApi,
}
: Dependencies & KubeObjectMetaProps) => {
if (!object) {
return null;
}
@@ -59,6 +65,10 @@ const NonInjectedKubeObjectMeta = observer(({
getFinalizers, getId, getName, metadata: { creationTimestamp },
} = object;
const ownerRefs = object.getOwnerRefs();
const namespace = getNs();
const namespaceDetailsUrl = namespace ? getDetailsUrl(
namespaceApi.formatUrlForNotListing({ name: namespace }),
) : "";
return (
<>
@@ -71,8 +81,8 @@ const NonInjectedKubeObjectMeta = observer(({
{getName()}
<KubeObjectStatusIcon key="icon" object={object} />
</DrawerItem>
<DrawerItem name="Namespace" hidden={isHidden("namespace") || !getNs()}>
{getNs()}
<DrawerItem name="Namespace" hidden={isHidden("namespace") || !namespace}>
<Link to={namespaceDetailsUrl}>{namespace}</Link>
</DrawerItem>
<DrawerItem name="UID" hidden={isHidden("uid")}>
{getId()}
@@ -122,5 +132,6 @@ export const KubeObjectMeta = withInjectables<Dependencies, KubeObjectMetaProps>
getDetailsUrl: di.inject(getDetailsUrlInjectable),
apiManager: di.inject(apiManagerInjectable),
logger: di.inject(loggerInjectable),
namespaceApi: di.inject(namespaceApiInjectable),
}),
});
@@ -15,6 +15,7 @@ export interface ClusterPageMenuRegistration {
title: React.ReactNode;
components: ClusterPageMenuComponents;
visible?: IComputedValue<boolean>;
orderNumber?: number;
}
export interface ClusterPageMenuComponents {
@@ -57,7 +57,7 @@ const extensionSidebarItemRegistratorInjectable = getInjectable({
const res: SidebarItemRegistration = {
id: `${extension.sanitizedExtensionId}-${registration.id}`,
orderNumber: 9999,
orderNumber: registration.orderNumber ?? 9999,
parentId: registration.parentId
? `${extension.sanitizedExtensionId}-${registration.parentId}`
+1 -1
View File
@@ -36,7 +36,7 @@ const webpackLensMain = (): webpack.Configuration => {
extensions: [".json", ".js", ".ts"],
},
externals: [
nodeExternals(),
nodeExternals({ modulesFromFile: true }),
],
module: {
parser: {
+1 -1
View File
@@ -46,7 +46,7 @@ export function webpackLensRenderer(): webpack.Configuration {
],
},
externals: [
nodeExternals(),
nodeExternals({ modulesFromFile: true }),
],
optimization: {
minimize: false,
File diff suppressed because it is too large. Load diff