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 authored and GitHub committed 2022-05-16 07:17:57 -04:00
1 parent 381d77c633
commit dfcb7c3330
1062 files changed
+26512 -28689

No files matched your search

@@ -4,11 +4,12 @@
*/
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import hostedClusterIdInjectable from "../../../../common/cluster-store/hosted-cluster-id.injectable";
import ipcRendererInjectable from "../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DockStore, DockTab } from "../dock/store";
import { TabKind } from "../dock/store";
import dockStoreInjectable from "../dock/store.injectable";
import fse from "fs-extra";
const initialTabs: DockTab[] = [
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false },
@@ -24,20 +25,14 @@ describe("DockStore", () => {
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(
directoryForUserDataInjectable,
() => "some-test-suite-specific-directory-for-user-data",
);
await di.runSetups();
di.override(hostedClusterIdInjectable, () => "some-cluster-id");
di.override(ipcRendererInjectable, () => ({
on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
dockStore = di.inject(dockStoreInjectable);
await dockStore.whenReady;
});
afterEach(() => {
fse.remove("some-test-suite-specific-directory-for-user-data");
dockStore.closeAllTabs();
});
it("closes tab and selects one from right", () => {
@@ -11,18 +11,16 @@ import { DockTabs } from "../dock-tabs";
import type { DockStore, DockTab } from "../dock/store";
import { TabKind } from "../dock/store";
import { noop } from "../../../utils";
import { ThemeStore } from "../../../theme.store";
import { UserStore } from "../../../../common/user-store";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import dockStoreInjectable from "../dock/store.injectable";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import directoryForUserDataInjectable
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable
from "../../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable
from "../../../../common/get-configuration-file-model/app-version/app-version.injectable";
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../../../../common/get-configuration-file-model/app-version/app-version.injectable";
import assert from "assert";
import hostedClusterIdInjectable from "../../../../common/cluster-store/hosted-cluster-id.injectable";
import ipcRendererInjectable from "../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
jest.mock("electron", () => ({
app: {
@@ -80,10 +78,15 @@ describe("<DockTabs />", () => {
render = renderFor(di);
di.override(hostedClusterIdInjectable, () => "some-cluster-id");
di.override(
directoryForUserDataInjectable,
() => "some-test-suite-specific-directory-for-user-data",
);
di.override(ipcRendererInjectable, () => ({
on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
@@ -92,16 +95,11 @@ describe("<DockTabs />", () => {
dockStore = di.inject(dockStoreInjectable);
UserStore.createInstance();
ThemeStore.createInstance();
await dockStore.whenReady;
dockStore.tabs = initialTabs;
});
afterEach(() => {
ThemeStore.resetInstance();
UserStore.resetInstance();
// TODO: A unit test may not cause side effects. Here accessing file system is a side effect.
fse.remove("some-test-suite-specific-directory-for-user-data");
});
@@ -123,6 +121,8 @@ describe("<DockTabs />", () => {
const { container, getByText } = render(getComponent(dockStore));
const tab = container.querySelector(".Tab");
assert(tab);
fireEvent.contextMenu(tab);
expect(getByText("Close all tabs")).toBeInTheDocument();
});
@@ -134,6 +134,8 @@ describe("<DockTabs />", () => {
const tab = container.querySelector(".Tab");
assert(tab);
fireEvent.contextMenu(tab);
fireEvent.click(getByText("Close"));
@@ -169,6 +171,8 @@ describe("<DockTabs />", () => {
const { container, getByText, rerender } = render(getComponent(dockStore));
const tab = container.querySelector(".Tab");
assert(tab);
fireEvent.contextMenu(tab);
const command = getByText("Close all tabs");
@@ -199,6 +203,8 @@ describe("<DockTabs />", () => {
const { container, getByText } = render(getComponent(dockStore));
const tab = container.querySelector(".Tab");
assert(tab);
fireEvent.contextMenu(tab);
const closeAll = getByText("Close all tabs");
const closeOthers = getByText("Close other tabs");
@@ -4,9 +4,9 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import type { GroupSelectOption, SelectOption } from "../../select";
import userCreateResourceTemplatesInjectable from "./user-templates.injectable";
import lensCreateResourceTemplatesInjectable from "./lens-templates.injectable";
import type { GroupBase } from "react-select";
export type RawTemplates = [group: string, items: [file: string, contents: string][]];
@@ -17,7 +17,7 @@ const createResourceTemplatesInjectable = getInjectable({
const lensResourceTemplates = await di.inject(lensCreateResourceTemplatesInjectable);
const userResourceTemplates = di.inject(userCreateResourceTemplatesInjectable);
return computed(() => {
return computed((): GroupBase<{ label: string; value: string }>[] => {
const res = [
...userResourceTemplates.get(),
lensResourceTemplates,
@@ -26,7 +26,7 @@ const createResourceTemplatesInjectable = getInjectable({
return res.map(([group, items]) => ({
label: group,
options: items.map(([label, value]) => ({ label, value })),
}) as GroupSelectOption<SelectOption<string>>);
}));
});
},
});
@@ -3,16 +3,14 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { StorageHelper } from "../../../utils";
import type { DockTabStorageState } from "../dock-tab-store/dock-tab.store";
import type { DockTabStoreDependencies } from "../dock-tab-store/dock-tab.store";
import { DockTabStore } from "../dock-tab-store/dock-tab.store";
interface Dependencies {
createStorage:<T> (storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>;
export interface CreateResourceTabStoreDependencies extends DockTabStoreDependencies {
}
export class CreateResourceTabStore extends DockTabStore<string> {
constructor(protected dependencies: Dependencies) {
constructor(protected readonly dependencies: CreateResourceTabStoreDependencies) {
super(dependencies, {
storageKey: "create_resource",
});
@@ -7,7 +7,7 @@ import type { IComputedValue } from "mobx";
import { computed, observable } from "mobx";
import path from "path";
import os from "os";
import { delay, getOrInsert, waitForPath } from "../../../utils";
import { delay, getOrInsert, isErrnoException, waitForPath } from "../../../utils";
import { watch } from "chokidar";
import { readFile } from "fs/promises";
import logger from "../../../../common/logger";
@@ -48,7 +48,7 @@ function watchUserCreateResourceTemplates(): IComputedValue<RawTemplates[]> {
templates.set(filePath, contents);
} catch (error) {
if (error?.code === "ENOENT") {
if (isErrnoException(error) && error.code === "ENOENT") {
// ignore, file disappeared
} else {
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while reading ${filePath}`, error);
@@ -4,7 +4,7 @@
*/
import React from "react";
import type { GroupSelectOption, SelectOption } from "../../select";
import type { SelectOption } from "../../select";
import { Select } from "../../select";
import yaml from "js-yaml";
import type { IComputedValue } from "mobx";
@@ -17,23 +17,29 @@ import { InfoPanel } from "../info-panel";
import * as resourceApplierApi from "../../../../common/k8s-api/endpoints/resource-applier.api";
import { Notifications } from "../../notifications";
import logger from "../../../../common/logger";
import type { KubeJsonApiData } from "../../../../common/k8s-api/kube-json-api";
import { getDetailsUrl } from "../../kube-detail-params";
import { apiManager } from "../../../../common/k8s-api/api-manager";
import { prevDefault } from "../../../utils";
import { navigate } from "../../../navigation";
import type { ApiManager } from "../../../../common/k8s-api/api-manager";
import { isObject, prevDefault } from "../../../utils";
import { withInjectables } from "@ogre-tools/injectable-react";
import createResourceTabStoreInjectable from "./store.injectable";
import createResourceTemplatesInjectable from "./create-resource-templates.injectable";
import { Spinner } from "../../spinner";
import type { GroupBase } from "react-select";
import type { Navigate } from "../../../navigation/navigate.injectable";
import type { GetDetailsUrl } from "../../kube-detail-params/get-details-url.injectable";
import apiManagerInjectable from "../../../../common/k8s-api/api-manager/manager.injectable";
import getDetailsUrlInjectable from "../../kube-detail-params/get-details-url.injectable";
import navigateInjectable from "../../../navigation/navigate.injectable";
export interface CreateResourceProps {
tab: DockTab;
}
interface Dependencies {
createResourceTemplates: IComputedValue<GroupSelectOption<SelectOption>[]>;
createResourceTemplates: IComputedValue<GroupBase<{ label: string; value: string }>[]>;
createResourceTabStore: CreateResourceTabStore;
apiManager: ApiManager;
navigate: Navigate;
getDetailsUrl: GetDetailsUrl;
}
@observer
@@ -50,7 +56,7 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
}
get data() {
return this.props.createResourceTabStore.getData(this.tabId);
return this.props.createResourceTabStore.getData(this.tabId) ?? "";
}
onChange = (value: string) => {
@@ -62,26 +68,24 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
this.error = error.toString();
};
onSelectTemplate = (item: SelectOption<string>) => {
this.props.createResourceTabStore.setData(this.tabId, item.value);
};
create = async (): Promise<void> => {
if (this.error || !this.data.trim()) {
const { apiManager, getDetailsUrl, navigate } = this.props;
if (this.error || !this.data?.trim()) {
// do not save when field is empty or there is an error
return;
}
// skip empty documents
const resources = yaml.loadAll(this.data).filter(Boolean);
const resources = yaml.loadAll(this.data).filter(isObject);
if (resources.length === 0) {
return void logger.info("Nothing to create");
}
const creatingResources = resources.map(async (resource: string) => {
const creatingResources = resources.map(async (resource) => {
try {
const data = await resourceApplierApi.update(resource) as KubeJsonApiData;
const data = await resourceApplierApi.update(resource);
const { kind, apiVersion, metadata: { name, namespace }} = data;
const showDetails = () => {
@@ -93,11 +97,16 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
const close = Notifications.ok(
<p>
{kind} <a onClick={prevDefault(showDetails)}>{name}</a> successfully created.
{kind}
{" "}
<a onClick={prevDefault(showDetails)}>
{name}
</a>
{" successfully created."}
</p>,
);
} catch (error) {
Notifications.error(error?.toString() ?? "Unknown error occured");
Notifications.checkedError(error, "Unknown error occured while creating resources");
}
});
@@ -107,16 +116,20 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
renderControls() {
return (
<div className="flex gaps align-center">
<Select
<Select<string, SelectOption<string>, false>
id="create-resource-resource-templates-input"
autoConvertOptions={false}
controlShouldRenderValue={false} // always keep initial placeholder
className="TemplateSelect"
placeholder="Select Template ..."
options={this.props.createResourceTemplates.get()}
formatGroupLabel={group => group.label}
menuPlacement="top"
themeName="outlined"
onChange={ this.onSelectTemplate}
onChange={(option) => {
if (option) {
this.props.createResourceTabStore.setData(this.tabId, option.value);
}
}}
/>
</div>
);
@@ -150,8 +163,11 @@ export const CreateResource = withInjectables<Dependencies, CreateResourceProps>
getPlaceholder: () => <Spinner center />,
getProps: async (di, props) => ({
...props,
createResourceTabStore: di.inject(createResourceTabStoreInjectable),
createResourceTemplates: await di.inject(createResourceTemplatesInjectable),
...props,
apiManager: di.inject(apiManagerInjectable),
getDetailsUrl: di.inject(getDetailsUrlInjectable),
navigate: di.inject(navigateInjectable),
}),
});
@@ -4,8 +4,9 @@
*/
import { action, observable, reaction } from "mobx";
import type { StorageHelper } from "../../../utils";
import type { StorageLayer } from "../../../utils";
import { autoBind, toJS } from "../../../utils";
import type { CreateStorage } from "../../../utils/create-storage/create-storage";
import type { TabId } from "../dock/store";
export interface DockTabStoreOptions {
@@ -15,37 +16,26 @@ export interface DockTabStoreOptions {
export type DockTabStorageState<T> = Record<TabId, T>;
interface DockTabStoreDependencies {
createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>;
export interface DockTabStoreDependencies {
createStorage: CreateStorage;
}
export class DockTabStore<T> {
protected storage?: StorageHelper<DockTabStorageState<T>>;
private data = observable.map<TabId, T>();
protected readonly storage?: StorageLayer<DockTabStorageState<T>>;
private readonly data = observable.map<TabId, T>();
constructor(protected dependencies: DockTabStoreDependencies, protected options: DockTabStoreOptions) {
constructor(protected readonly dependencies: DockTabStoreDependencies, protected readonly options: DockTabStoreOptions) {
autoBind(this);
this.options.autoInit ??= true;
this.options = {
autoInit: true,
...this.options,
};
const { storageKey, autoInit } = this.options;
if (this.options.autoInit) {
this.init();
}
}
if (autoInit && storageKey) {
const storage = this.storage = this.dependencies.createStorage(storageKey, {});
protected init() {
const { storageKey } = this.options;
// auto-save to local-storage
if (storageKey) {
this.storage = this.dependencies.createStorage<T>(storageKey, {});
this.storage.whenReady.then(() => {
this.data.replace(this.storage.value);
reaction(() => this.toJSON(), data => this.storage.set(data));
storage.whenReady.then(() => {
this.data.replace(storage.value);
reaction(() => this.toJSON(), data => storage.set(data));
});
}
}
+52 -53
View File
@@ -13,7 +13,7 @@ import type { TabProps } from "../tabs";
import { Tab } from "../tabs";
import { Icon } from "../icon";
import { Menu, MenuItem } from "../menu";
import { observable, makeObservable } from "mobx";
import { observable } from "mobx";
import { isMac } from "../../../common/vars";
import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "./dock/store.injectable";
@@ -29,48 +29,43 @@ interface Dependencies {
@observer
class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
@observable menuVisible = false;
private readonly menuVisible = observable.box(false);
constructor(props: DockTabProps & Dependencies) {
super(props);
makeObservable(this);
autoBind(this);
}
get tabId() {
return this.props.value.id;
close(id: string) {
this.props.dockStore.closeTab(id);
}
close() {
this.props.dockStore.closeTab(this.tabId);
}
renderMenu() {
renderMenu(tabId: string) {
const { closeTab, closeAllTabs, closeOtherTabs, closeTabsToTheRight, tabs, getTabIndex } = this.props.dockStore;
const closeAllDisabled = tabs.length === 1;
const closeOtherDisabled = tabs.length === 1;
const closeRightDisabled = getTabIndex(this.tabId) === tabs.length - 1;
const closeRightDisabled = getTabIndex(tabId) === tabs.length - 1;
return (
<Menu
usePortal
htmlFor={`tab-${this.tabId}`}
htmlFor={`tab-${tabId}`}
className="DockTabMenu"
isOpen={this.menuVisible}
open={() => this.menuVisible = true}
close={() => this.menuVisible = false}
isOpen={this.menuVisible.get()}
open={() => this.menuVisible.set(true)}
close={() => this.menuVisible.set(false)}
toggleEvent="contextmenu"
>
<MenuItem onClick={() => closeTab(this.tabId)}>
<MenuItem onClick={() => closeTab(tabId)}>
Close
</MenuItem>
<MenuItem onClick={() => closeAllTabs()} disabled={closeAllDisabled}>
Close all tabs
</MenuItem>
<MenuItem onClick={() => closeOtherTabs(this.tabId)} disabled={closeOtherDisabled}>
<MenuItem onClick={() => closeOtherTabs(tabId)} disabled={closeOtherDisabled}>
Close other tabs
</MenuItem>
<MenuItem onClick={() => closeTabsToTheRight(this.tabId)} disabled={closeRightDisabled}>
<MenuItem onClick={() => closeTabsToTheRight(tabId)} disabled={closeRightDisabled}>
Close tabs to the right
</MenuItem>
</Menu>
@@ -79,52 +74,56 @@ class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
render() {
const { className, moreActions, dockStore, active, ...tabProps } = this.props;
const { title, pinned } = tabProps.value;
const label = (
<div className="flex align-center" onAuxClick={isMiddleClick(prevDefault(this.close))}>
<span className={styles.title}>{title}</span>
{moreActions}
{!pinned && (
<div className={styles.close}>
<Icon
small material="close"
tooltip={`Close ${isMac ? "⌘+W" : "Ctrl+W"}`}
onClick={prevDefault(this.close)}
/>
</div>
)}
<Tooltip
targetId={`tab-${this.tabId}`}
preferredPositions={[TooltipPosition.TOP, TooltipPosition.TOP_LEFT]}
style={{ transitionDelay: "700ms" }}
>{title}</Tooltip>
</div>
);
if (!tabProps.value) {
return;
}
const { title, pinned, id } = tabProps.value;
const close = prevDefault(() => this.close(id));
return (
<>
<Tab
{...tabProps}
id={`tab-${this.tabId}`}
id={`tab-${id}`}
className={cssNames(styles.DockTab, className, {
[styles.pinned]: pinned,
})}
onContextMenu={() => this.menuVisible = true}
label={label}
onContextMenu={() => this.menuVisible.set(true)}
label={(
<div className="flex align-center" onAuxClick={isMiddleClick(close)}>
<span className={styles.title}>{title}</span>
{moreActions}
{!pinned && (
<div className={styles.close}>
<Icon
small
material="close"
tooltip={`Close ${isMac ? "⌘+W" : "Ctrl+W"}`}
onClick={close}
/>
</div>
)}
<Tooltip
targetId={`tab-${id}`}
preferredPositions={[TooltipPosition.TOP, TooltipPosition.TOP_LEFT]}
style={{ transitionDelay: "700ms" }}
>
{title}
</Tooltip>
</div>
)}
/>
{this.renderMenu()}
{this.renderMenu(id)}
</>
);
}
}
export const DockTab = withInjectables<Dependencies, DockTabProps>(
NonInjectedDockTab,
{
getProps: (di, props) => ({
dockStore: di.inject(dockStoreInjectable),
...props,
}),
},
);
export const DockTab = withInjectables<Dependencies, DockTabProps>(NonInjectedDockTab, {
getProps: (di, props) => ({
dockStore: di.inject(dockStoreInjectable),
...props,
}),
});
+12 -6
View File
@@ -17,17 +17,17 @@ import { cssVar } from "../../utils";
export interface DockTabsProps {
tabs: DockTabModel[];
autoFocus: boolean;
selectedTab: DockTabModel;
selectedTab: DockTabModel | undefined;
onChangeTab: (tab: DockTabModel) => void;
}
export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: DockTabsProps) => {
const elem = useRef<HTMLDivElement>();
const elem = useRef<HTMLDivElement | null>(null);
const minTabSize = useRef<number>(0);
const [showScrollbar, setShowScrollbar] = useState(false);
const getTabElements = (): HTMLDivElement[] => {
return Array.from(elem.current?.querySelectorAll(".Tabs .Tab"));
return Array.from(elem.current?.querySelectorAll(".Tabs .Tab") ?? []);
};
const renderTab = (tab?: DockTabModel) => {
@@ -70,9 +70,11 @@ export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: DockTabs
};
useEffect(() => {
const cssVars = cssVar(elem.current);
if (elem.current) {
const cssVars = cssVar(elem.current);
minTabSize.current = cssVars.get("--min-tab-width").valueOf();
minTabSize.current = cssVars.get("--min-tab-width").valueOf();
}
});
useResizeObserver(elem.current, () => {
@@ -81,7 +83,11 @@ export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: DockTabs
});
return (
<div className={styles.dockTabs} ref={elem} role="tablist">
<div
className={styles.dockTabs}
ref={elem}
role="tablist"
>
<Tabs
autoFocus={autoFocus}
value={selectedTab}
+18 -10
View File
@@ -43,7 +43,7 @@ enum Direction {
@observer
class NonInjectedDock extends React.Component<DockProps & Dependencies> {
private element = React.createRef<HTMLDivElement>();
private readonly element = React.createRef<HTMLDivElement>();
componentDidMount() {
document.addEventListener("keydown", this.onKeyDown);
@@ -58,7 +58,7 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
const { code, ctrlKey, metaKey, shiftKey } = evt;
// Determine if user working inside <Dock/> or using any other areas in app
const dockIsFocused = this.element?.current.contains(document.activeElement);
const dockIsFocused = this.element.current?.contains(document.activeElement);
if (!selectedTab || !dockIsFocused) return;
@@ -68,15 +68,15 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
if ((ctrlKey && code === "KeyW") || (metaKey && code === "KeyW")) {
closeTab(selectedTab.id);
this.element?.current.focus(); // Avoid loosing focus when closing tab
this.element.current?.focus(); // Avoid loosing focus when closing tab
}
if(ctrlKey && code === "Period") {
this.switchToNextTab();
this.switchToNextTab(selectedTab, Direction.NEXT);
}
if(ctrlKey && code === "Comma") {
this.switchToNextTab(Direction.PREV);
this.switchToNextTab(selectedTab, Direction.PREV);
}
};
@@ -85,11 +85,11 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
open();
selectTab(tab.id);
this.element?.current.focus();
this.element.current?.focus();
};
switchToNextTab = (direction: Direction = Direction.NEXT) => {
const { tabs, selectedTab } = this.props.dockStore;
switchToNextTab = (selectedTab: DockTab, direction: Direction) => {
const { tabs } = this.props.dockStore;
const currentIndex = tabs.indexOf(selectedTab);
const nextIndex = currentIndex + direction;
@@ -160,9 +160,17 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
/>
<div className={cssNames("toolbar flex gaps align-center box grow", { "pl-0": tabs.length == 0 })}>
<div className="dock-menu box grow">
<MenuActions usePortal triggerIcon={{ material: "add", className: "new-dock-tab", tooltip: "New tab" }} closeOnScroll={false}>
<MenuActions
usePortal
triggerIcon={{ material: "add", className: "new-dock-tab", tooltip: "New tab" }}
closeOnScroll={false}
>
<MenuItem className="create-terminal-tab" onClick={() => this.props.createTerminalTab()}>
<Icon small svg="terminal" size={15} />
<Icon
small
svg="terminal"
size={15}
/>
Terminal session
</MenuItem>
<MenuItem className="create-resource-tab" onClick={() => this.props.createResourceTab()}>
@@ -23,6 +23,7 @@ const dockStorageInjectable = getInjectable({
pinned: false,
},
],
isOpen: false,
});
},
});
+10 -8
View File
@@ -5,7 +5,7 @@
import * as uuid from "uuid";
import { action, comparer, computed, makeObservable, observable, reaction, runInAction } from "mobx";
import type { StorageHelper } from "../../../utils";
import type { StorageLayer } from "../../../utils";
import { autoBind } from "../../../utils";
import throttle from "lodash/throttle";
@@ -70,7 +70,7 @@ export interface DockStorageState {
height: number;
tabs: DockTab[];
selectedTabId?: TabId;
isOpen?: boolean;
isOpen: boolean;
}
export interface DockTabChangeEvent {
@@ -99,7 +99,7 @@ export interface DockTabCloseEvent {
}
interface Dependencies {
readonly storage: StorageHelper<DockStorageState>;
readonly storage: StorageLayer<DockStorageState>;
readonly tabDataClearers: Record<TabKind, (tabId: TabId) => void>;
readonly tabDataValidator: Partial<Record<TabKind, (tabId: TabId) => boolean>>;
}
@@ -157,7 +157,7 @@ export class DockStore implements DockStorageState {
);
}
set selectedTabId(tabId: TabId) {
set selectedTabId(tabId: TabId | undefined) {
if (tabId && !this.getTabById(tabId)) return; // skip invalid ids
this.dependencies.storage.merge({ selectedTabId: tabId });
@@ -283,7 +283,7 @@ export class DockStore implements DockStorageState {
const tabNumbers = this.tabs
.filter(tab => tab.kind === kind)
.map(tab => {
const tabNumber = +tab.title.match(/\d+/);
const tabNumber = Number(tab.title.match(/\d+/));
return tabNumber === 0 ? 1 : tabNumber; // tab without a number is first
});
@@ -343,7 +343,7 @@ export class DockStore implements DockStorageState {
this.selectTab(newTab.id);
} else {
this.selectedTabId = null;
this.selectedTabId = undefined;
this.close();
}
}
@@ -375,12 +375,14 @@ export class DockStore implements DockStorageState {
renameTab(tabId: TabId, title: string) {
const tab = this.getTabById(tabId);
tab.title = title;
if (tab) {
tab.title = title;
}
}
@action
selectTab(tabId: TabId) {
this.selectedTabId = this.getTabById(tabId)?.id ?? null;
this.selectedTabId = this.getTabById(tabId)?.id;
}
@action
@@ -5,12 +5,14 @@
import { getInjectable } from "@ogre-tools/injectable";
import { EditResourceTabStore } from "./store";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
import apiManagerInjectable from "../../../../common/k8s-api/api-manager/manager.injectable";
const editResourceTabStoreInjectable = getInjectable({
id: "edit-resource-tab-store",
instantiate: (di) => new EditResourceTabStore({
createStorage: di.inject(createStorageInjectable),
apiManager: di.inject(apiManagerInjectable),
}),
});
@@ -3,12 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { StorageHelper } from "../../../utils";
import type { DockTabStorageState } from "../dock-tab-store/dock-tab.store";
import type { DockTabStoreDependencies } from "../dock-tab-store/dock-tab.store";
import { DockTabStore } from "../dock-tab-store/dock-tab.store";
import type { TabId } from "../dock/store";
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
import { apiManager } from "../../../../common/k8s-api/api-manager";
import type { ApiManager } from "../../../../common/k8s-api/api-manager";
import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
export interface EditingResource {
@@ -17,12 +16,12 @@ export interface EditingResource {
firstDraft?: string;
}
interface Dependencies {
createStorage:<T> (storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>;
export interface EditResourceTabStoreDependencies extends DockTabStoreDependencies {
readonly apiManager: ApiManager;
}
export class EditResourceTabStore extends DockTabStore<EditingResource> {
constructor(protected dependencies: Dependencies) {
constructor(protected readonly dependencies: EditResourceTabStoreDependencies) {
super(dependencies, {
storageKey: "edit_resource_store",
});
@@ -36,19 +35,27 @@ export class EditResourceTabStore extends DockTabStore<EditingResource> {
return super.isReady(tabId) && Boolean(this.getResource(tabId)); // ready to edit resource
}
getStore(tabId: TabId): KubeObjectStore<KubeObject> | undefined {
return apiManager.getStore(this.getResourcePath(tabId));
getStore(tabId: TabId): KubeObjectStore | undefined {
const apiPath = this.getResourcePath(tabId);
return apiPath
? this.dependencies.apiManager.getStore(apiPath)
: undefined;
}
getResource(tabId: TabId): KubeObject | undefined {
return this.getStore(tabId)?.getByPath(this.getResourcePath(tabId));
const apiPath = this.getResourcePath(tabId);
return apiPath
? this.dependencies.apiManager.getStore(apiPath)?.getByPath(apiPath)
: undefined;
}
getResourcePath(tabId: TabId): string | undefined {
return this.getData(tabId)?.resource;
}
getTabIdByResource(object: KubeObject): TabId {
getTabIdByResource(object: KubeObject): string | undefined {
return this.findTabIdFromData(({ resource }) => object.selfLink === resource);
}
@@ -4,11 +4,11 @@
*/
import React from "react";
import { autorun, computed, makeObservable, observable } from "mobx";
import { autorun, makeObservable, observable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import yaml from "js-yaml";
import type { DockTab, TabId } from "../dock/store";
import type { EditResourceTabStore } from "./store";
import type { EditingResource, EditResourceTabStore } from "./store";
import { InfoPanel } from "../info-panel";
import { Badge } from "../../badge";
import { EditorPanel } from "../editor-panel";
@@ -17,8 +17,9 @@ import type { KubeObject } from "../../../../common/k8s-api/kube-object";
import { createPatch } from "rfc6902";
import { withInjectables } from "@ogre-tools/injectable-react";
import editResourceTabStoreInjectable from "./store.injectable";
import { noop } from "../../../utils";
import { noop, onceDefined } from "../../../utils";
import closeDockTabInjectable from "../dock/close-dock-tab.injectable";
import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
export interface EditResourceProps {
tab: DockTab;
@@ -29,9 +30,16 @@ interface Dependencies {
closeTab: (tabId: TabId) => void;
}
interface SaveDraftArgs {
tabData: EditingResource;
resource: KubeObject;
store: KubeObjectStore;
}
@observer
class NonInjectedEditResource extends React.Component<EditResourceProps & Dependencies> {
@observable error = "";
@observable draft = "";
constructor(props: EditResourceProps & Dependencies) {
super(props);
@@ -40,13 +48,32 @@ class NonInjectedEditResource extends React.Component<EditResourceProps & Depend
componentDidMount(): void {
disposeOnUnmount(this, [
autorun(() => {
const store = this.props.editResourceStore.getStore(this.props.tab.id);
const tabData = this.props.editResourceStore.getData(this.props.tab.id);
const obj = this.resource;
onceDefined(
() => {
const tabData = this.tabData;
const resource = this.resource;
if (!obj) {
if (store?.isLoaded) {
if (tabData && resource) {
return { tabData, resource };
}
return undefined;
},
({ tabData, resource }) => {
if (typeof tabData.draft === "string") {
this.draft = tabData.draft;
} else {
this.draft = tabData.firstDraft = yaml.dump(resource.toPlainObject());
}
},
),
autorun(() => {
const store = this.store;
const tabData = this.tabData;
const resource = this.resource;
if (!resource && store && tabData) {
if (store.isLoaded) {
// auto-close tab when resource removed from store
this.props.closeTab(this.props.tab.id);
} else if (!store.isLoading) {
@@ -62,68 +89,45 @@ class NonInjectedEditResource extends React.Component<EditResourceProps & Depend
return this.props.tab.id;
}
get isReadyForEditing() {
return this.props.editResourceStore.isReady(this.tabId);
get store() {
return this.props.editResourceStore.getStore(this.props.tab.id);
}
get resource(): KubeObject | undefined {
get resource() {
return this.props.editResourceStore.getResource(this.tabId);
}
@computed get draft(): string {
if (!this.isReadyForEditing) {
return ""; // wait until tab's data and kube-object resource are loaded
}
const editData = this.props.editResourceStore.getData(this.tabId);
if (typeof editData.draft === "string") {
return editData.draft;
}
const firstDraft = yaml.dump(this.resource.toPlainObject()); // dump resource first time
return editData.firstDraft = firstDraft;
get tabData() {
return this.props.editResourceStore.getData(this.tabId);
}
saveDraft(draft: string) {
this.props.editResourceStore.getData(this.tabId).draft = draft;
}
onChange = (draft: string) => {
this.error = ""; // reset first
this.saveDraft(draft);
};
onError = (error?: Error | string) => {
this.error = error.toString();
};
save = async () => {
async save({ resource, store, tabData }: SaveDraftArgs) {
if (this.error) {
return null;
}
const store = this.props.editResourceStore.getStore(this.tabId);
const currentVersion = yaml.load(this.draft);
const firstVersion = yaml.load(this.props.editResourceStore.getData(this.tabId).firstDraft ?? this.draft);
const firstVersion = yaml.load(tabData.firstDraft ?? this.draft);
const patches = createPatch(firstVersion, currentVersion);
const updatedResource = await store.patch(this.resource, patches);
const updatedResource = await store.patch(resource, patches);
this.props.editResourceStore.clearInitialDraft(this.tabId);
return (
<p>
{updatedResource.kind} <b>{updatedResource.getName()}</b> updated.
{updatedResource.kind}
{" "}
<b>{updatedResource.getName()}</b>
{" updated."}
</p>
);
};
}
render() {
const { tabId, error, onChange, onError, save, draft, isReadyForEditing, resource } = this;
const { tabId, error, draft, tabData, resource, store } = this;
if (!isReadyForEditing) {
return <Spinner center/>;
if (!tabData || !resource || !store) {
return <Spinner center />;
}
return (
@@ -131,22 +135,28 @@ class NonInjectedEditResource extends React.Component<EditResourceProps & Depend
<InfoPanel
tabId={tabId}
error={error}
submit={save}
submit={() => this.save({ resource, store, tabData })}
submitLabel="Save"
submittingMessage="Applying.."
controls={(
<div className="resource-info flex gaps align-center">
<span>Kind:</span><Badge label={resource.kind}/>
<span>Name:</span><Badge label={resource.getName()}/>
<span>Namespace:</span><Badge label={resource.getNs() || "global"}/>
<span>Kind:</span>
<Badge label={resource.kind} />
<span>Name:</span>
<Badge label={resource.getName()} />
<span>Namespace:</span>
<Badge label={resource.getNs() || "global"} />
</div>
)}
/>
<EditorPanel
tabId={tabId}
value={draft}
onChange={onChange}
onError={onError}
onChange={draft => {
this.error = "";
tabData.draft = draft;
}}
onError={error => this.error = String(error)}
/>
</div>
);
+44 -55
View File
@@ -5,13 +5,13 @@
import styles from "./editor-panel.module.scss";
import throttle from "lodash/throttle";
import React from "react";
import { makeObservable, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React, { createRef, useEffect } from "react";
import { reaction } from "mobx";
import { observer } from "mobx-react";
import type { DockStore, TabId } from "./dock/store";
import { cssNames } from "../../utils";
import type { MonacoEditorProps } from "../monaco-editor";
import { cssNames, disposer } from "../../utils";
import { MonacoEditor } from "../monaco-editor";
import type { MonacoEditorProps, MonacoEditorRef } from "../monaco-editor";
import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "./dock/store.injectable";
@@ -28,59 +28,48 @@ interface Dependencies {
dockStore: DockStore;
}
const defaultProps: Partial<EditorPanelProps> = {
autoFocus: true,
};
const NonInjectedEditorPanel = observer(({
dockStore,
onChange,
tabId,
value,
autoFocus = true,
className,
onError,
}: Dependencies & EditorPanelProps) => {
const editor = createRef<MonacoEditorRef>();
@observer
class NonInjectedEditorPanel extends React.Component<EditorPanelProps & Dependencies> {
static defaultProps = defaultProps as object;
@observable.ref editor?: MonacoEditor;
constructor(props: EditorPanelProps & Dependencies) {
super(props);
makeObservable(this);
}
componentDidMount() {
disposeOnUnmount(this, [
// keep focus on editor's area when <Dock/> just opened
reaction(() => this.props.dockStore.isOpen, isOpen => isOpen && this.editor?.focus(), {
useEffect(() => disposer(
reaction(
() => dockStore.isOpen,
isOpen => isOpen && editor.current?.focus(),
{
fireImmediately: true,
}),
},
),
dockStore.onResize(throttle(() => editor.current?.focus(), 250)),
));
// focus to editor on dock's resize or turning into fullscreen mode
this.props.dockStore.onResize(throttle(() => this.editor?.focus(), 250)),
]);
if (!tabId) {
return null;
}
render() {
const { className, autoFocus, tabId, value, onChange, onError } = this.props;
return (
<MonacoEditor
autoFocus={autoFocus}
id={tabId}
value={value}
className={cssNames(styles.EditorPanel, className)}
onChange={onChange}
onError={onError}
ref={editor}
/>
);
});
if (!tabId) return null;
return (
<MonacoEditor
autoFocus={autoFocus}
id={tabId}
value={value}
className={cssNames(styles.EditorPanel, className)}
onChange={onChange}
onError={onError}
ref={monaco => this.editor = monaco}
/>
);
}
}
export const EditorPanel = withInjectables<Dependencies, EditorPanelProps>(
NonInjectedEditorPanel,
{
getProps: (di, props) => ({
dockStore: di.inject(dockStoreInjectable),
...props,
}),
},
);
export const EditorPanel = withInjectables<Dependencies, EditorPanelProps>(NonInjectedEditorPanel, {
getProps: (di, props) => ({
...props,
dockStore: di.inject(dockStoreInjectable),
}),
});
+22 -7
View File
@@ -20,7 +20,7 @@ import dockStoreInjectable from "./dock/store.injectable";
export interface InfoPanelProps extends OptionalProps {
tabId: TabId;
submit?: () => Promise<ReactNode | string | void>;
submit?: () => Promise<string | React.ReactElement | React.ReactElement[] | null | undefined | false | void>;
}
export interface OptionalProps {
@@ -79,11 +79,15 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
this.waiting = true;
try {
const result = await this.props.submit();
const result = await this.props.submit?.();
if (showNotifications && result) Notifications.ok(result);
if (showNotifications && result) {
Notifications.ok(result);
}
} catch (error) {
if (showNotifications) Notifications.error(error.toString());
if (showNotifications) {
Notifications.checkedError(error, "Unknown error while submitting");
}
} finally {
this.waiting = false;
}
@@ -122,12 +126,22 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
</div>
{showStatusPanel && (
<div className="flex gaps align-center">
{waiting ? <><Spinner /> {submittingMessage}</> : this.renderErrorIcon()}
{waiting ? (
<>
<Spinner />
{" "}
{submittingMessage}
</>
) : this.renderErrorIcon()}
</div>
)}
{showButtons && (
<>
<Button plain label="Cancel" onClick={close} />
<Button
plain
label="Cancel"
onClick={close}
/>
<Button
active
outlined={showSubmitClose}
@@ -138,7 +152,8 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
/>
{showSubmitClose && (
<Button
primary active
primary
active
label={`${submitLabel} & Close`}
onClick={submitAndClose}
disabled={isDisabled}
@@ -5,7 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable";
import { InstallChartTabStore } from "./store";
import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable";
import type { IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import type { HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const installChartTabStoreInjectable = getInjectable({
@@ -17,7 +17,7 @@ const installChartTabStoreInjectable = getInjectable({
return new InstallChartTabStore({
createStorage: di.inject(createStorageInjectable),
versionsStore: createDockTabStore<string[]>(),
detailsStore: createDockTabStore<IReleaseUpdateDetails>(),
detailsStore: createDockTabStore<HelmReleaseUpdateDetails>(),
});
},
});
@@ -3,13 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, makeObservable, when } from "mobx";
import { action, makeObservable } from "mobx";
import type { TabId } from "../dock/store";
import type { DockTabStorageState } from "../dock-tab-store/dock-tab.store";
import type { DockTabStoreDependencies } from "../dock-tab-store/dock-tab.store";
import { DockTabStore } from "../dock-tab-store/dock-tab.store";
import { getChartDetails, getChartValues } from "../../../../common/k8s-api/endpoints/helm-charts.api";
import type { IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import type { StorageHelper } from "../../../utils";
import type { HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import { waitUntilDefined } from "../../../../common/utils/wait";
export interface IChartInstallData {
name: string;
@@ -22,18 +22,14 @@ export interface IChartInstallData {
lastVersion?: boolean;
}
interface Dependencies {
createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>;
export interface InstallChartTabStoreDependencies extends DockTabStoreDependencies {
versionsStore: DockTabStore<string[]>;
detailsStore: DockTabStore<IReleaseUpdateDetails>;
detailsStore: DockTabStore<HelmReleaseUpdateDetails>;
}
export class InstallChartTabStore extends DockTabStore<IChartInstallData> {
constructor(protected dependencies: Dependencies) {
super(
dependencies,
{ storageKey: "install_charts" },
);
constructor(protected readonly dependencies: InstallChartTabStoreDependencies) {
super(dependencies, { storageKey: "install_charts" });
makeObservable(this);
}
@@ -48,24 +44,21 @@ export class InstallChartTabStore extends DockTabStore<IChartInstallData> {
@action
async loadData(tabId: string) {
const promises = [];
const data = await waitUntilDefined(() => this.getData(tabId));
await when(() => this.isReady(tabId));
if (!this.getData(tabId).values) {
if (!this.getData(tabId)?.values) {
promises.push(this.loadValues(tabId));
}
if (!this.versions.getData(tabId)) {
promises.push(this.loadVersions(tabId));
promises.push(this.loadVersions(tabId, data));
}
await Promise.all(promises);
}
@action
async loadVersions(tabId: TabId) {
const { repo, name, version } = this.getData(tabId);
private async loadVersions(tabId: TabId, { repo, name, version }: IChartInstallData) {
this.versions.clearData(tabId); // reset
const charts = await getChartDetails(repo, name, { version });
const versions = charts.versions.map(chartVersion => chartVersion.version);
@@ -75,7 +68,7 @@ export class InstallChartTabStore extends DockTabStore<IChartInstallData> {
@action
async loadValues(tabId: TabId, attempt = 0): Promise<void> {
const data = this.getData(tabId);
const data = await waitUntilDefined(() => this.getData(tabId));
const { repo, name, version } = data;
const values = await getChartValues(repo, name, version);
@@ -22,7 +22,7 @@ import type { SelectOption } from "../../select";
import { Select } from "../../select";
import { Input } from "../../input";
import { EditorPanel } from "../editor-panel";
import type { IReleaseCreatePayload, IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import type { HelmReleaseCreatePayload, HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import { withInjectables } from "@ogre-tools/injectable-react";
import installChartTabStoreInjectable from "./store.injectable";
import dockStoreInjectable from "../dock/store.injectable";
@@ -30,13 +30,15 @@ import createReleaseInjectable from "../../+helm-releases/create-release/create-
import { Notifications } from "../../notifications";
import type { NavigateToHelmReleases } from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import navigateToHelmReleasesInjectable from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import assert from "assert";
import type { SingleValue } from "react-select";
export interface InstallCharProps {
tab: DockTab;
}
interface Dependencies {
createRelease: (payload: IReleaseCreatePayload) => Promise<IReleaseUpdateDetails>;
createRelease: (payload: HelmReleaseCreatePayload) => Promise<HelmReleaseUpdateDetails>;
installChartStore: InstallChartTabStore;
dockStore: DockStore;
navigateToHelmReleases: NavigateToHelmReleases;
@@ -73,28 +75,25 @@ class NonInjectedInstallChart extends Component<InstallCharProps & Dependencies>
return this.props.installChartStore.details.getData(this.tabId);
}
viewRelease = () => {
const { release } = this.releaseDetails;
viewRelease = ({ release }: HelmReleaseUpdateDetails) => {
this.props.navigateToHelmReleases({
name: release.name,
namespace: release.namespace,
});
this.props.dockStore.closeTab(this.tabId);
};
save(data: Partial<IChartInstallData>) {
const chart = { ...this.chartData, ...data };
assert(this.chartData, "Cannot update data before data exists");
this.props.installChartStore.setData(this.tabId, chart);
this.props.installChartStore.setData(this.tabId, { ...this.chartData, ...data });
}
onVersionChange = (option: SelectOption) => {
const version = option.value;
this.save({ version, values: "" });
this.props.installChartStore.loadValues(this.tabId);
onVersionChange = (option: SingleValue<SelectOption<string>>) => {
if (option) {
this.save({ ...option, values: "" });
this.props.installChartStore.loadValues(this.tabId);
}
};
onChange = action((values: string) => {
@@ -106,51 +105,66 @@ class NonInjectedInstallChart extends Component<InstallCharProps & Dependencies>
this.error = error.toString();
});
onNamespaceChange = (opt: SelectOption) => {
this.save({ namespace: opt.value });
onNamespaceChange = (option: SingleValue<SelectOption<string>>) => {
if (option) {
this.save({ namespace: option.value });
}
};
onReleaseNameChange = (name: string) => {
this.save({ releaseName: name });
};
install = async () => {
const { repo, name, version, namespace, values, releaseName } = this.chartData;
install = async ({ repo, name, version, namespace, values = "", releaseName }: IChartInstallData) => {
const details = await this.props.createRelease({
name: releaseName || undefined,
chart: name,
repo, namespace, version, values,
repo,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
namespace: namespace!,
version,
values,
});
this.props.installChartStore.details.setData(this.tabId, details);
return (
<p>Chart Release <b>{details.release.name}</b> successfully created.</p>
<p>
{"Chart Release "}
<b>{details.release.name}</b>
{" successfully created."}
</p>
);
};
render() {
const { tabId, chartData, versions, install } = this;
const { tabId, chartData, versions, install, releaseDetails } = this;
if (chartData?.values === undefined || !versions) {
return <Spinner center/>;
return <Spinner center />;
}
if (this.releaseDetails) {
if (releaseDetails) {
return (
<div className="InstallChartDone flex column gaps align-center justify-center">
<p>
<Icon material="check" big sticker/>
<Icon
material="check"
big
sticker
/>
</p>
<p>Installation complete!</p>
<div className="flex gaps align-center">
<Button
autoFocus primary
autoFocus
primary
label="View Helm Release"
onClick={prevDefault(this.viewRelease)}
onClick={prevDefault(() => this.viewRelease(releaseDetails))}
/>
<Button
plain active
plain
active
label="Show Notes"
onClick={() => this.showNotes = true}
/>
@@ -159,53 +173,55 @@ class NonInjectedInstallChart extends Component<InstallCharProps & Dependencies>
title="Helm Chart Install"
isOpen={this.showNotes}
close={() => this.showNotes = false}
logs={this.releaseDetails.log}
logs={releaseDetails.log}
/>
</div>
);
}
const { repo, name, version, namespace, releaseName } = chartData;
const panelControls = (
<div className="install-controls flex gaps align-center">
<span>Chart</span>
<Badge label={`${repo}/${name}`} title="Repo/Name"/>
<span>Version</span>
<Select
id="chart-version-input"
className="chart-version"
value={version}
options={versions}
onChange={this.onVersionChange}
menuPlacement="top"
themeName="outlined"
/>
<span>Namespace</span>
<NamespaceSelect
id="install-chart-namespace-select-input"
showIcons={false}
menuPlacement="top"
themeName="outlined"
value={namespace}
onChange={this.onNamespaceChange}
/>
<Input
placeholder="Name (optional)"
title="Release name"
maxLength={50}
value={releaseName}
onChange={this.onReleaseNameChange}
/>
</div>
);
const versionOptions = versions.map(version => ({
value: version,
label: version,
}));
return (
<div className="InstallChart flex column">
<InfoPanel
tabId={tabId}
controls={panelControls}
controls={(
<div className="install-controls flex gaps align-center">
<span>Chart</span>
<Badge label={`${repo}/${name}`} title="Repo/Name" />
<span>Version</span>
<Select
className="chart-version"
value={version}
options={versionOptions}
onChange={this.onVersionChange}
menuPlacement="top"
themeName="outlined"
/>
<span>Namespace</span>
<NamespaceSelect
showIcons={false}
menuPlacement="top"
themeName="outlined"
value={namespace}
onChange={this.onNamespaceChange}
/>
<Input
placeholder="Name (optional)"
title="Release name"
maxLength={50}
value={releaseName}
onChange={this.onReleaseNameChange}
/>
</div>
)}
error={this.error}
submit={install}
submit={() => install(chartData)}
disableSubmit={!chartData.namespace}
submitLabel="Install"
submittingMessage="Installing..."
showSubmitClose={false}
@@ -6,11 +6,8 @@
import React from "react";
import "@testing-library/jest-dom/extend-expect";
import * as selectEvent from "react-select-event";
import { Pod } from "../../../../../common/k8s-api/endpoints";
import { LogResourceSelector } from "../resource-selector";
import { dockerPod, deploymentPod1, deploymentPod2 } from "./pod.mock";
import { ThemeStore } from "../../../../theme.store";
import { UserStore } from "../../../../../common/user-store";
import mockFs from "mock-fs";
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
import type { DiRender } from "../../../test-utils/renderFor";
@@ -22,10 +19,10 @@ import { LogTabViewModel } from "../logs-view-model";
import type { TabId } from "../../dock/store";
import userEvent from "@testing-library/user-event";
import { SearchStore } from "../../../../search-store/search-store";
import getConfigurationFileModelInjectable
from "../../../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable
from "../../../../../common/get-configuration-file-model/app-version/app-version.injectable";
import getConfigurationFileModelInjectable from "../../../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../../../../../common/get-configuration-file-model/app-version/app-version.injectable";
import assert from "assert";
import ipcRendererInjectable from "../../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
jest.mock("electron", () => ({
app: {
@@ -66,8 +63,8 @@ function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependen
});
}
const getOnePodViewModel = (tabId: TabId, deps: Partial<LogTabViewModelDependencies> = {}): LogTabViewModel => {
const selectedPod = new Pod(dockerPod);
function getOnePodViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependencies> = {}): LogTabViewModel {
const selectedPod = dockerPod;
return mockLogTabViewModel(tabId, {
getLogTabData: () => ({
@@ -86,11 +83,11 @@ const getOnePodViewModel = (tabId: TabId, deps: Partial<LogTabViewModelDependenc
},
...deps,
});
};
}
const getFewPodsTabData = (tabId: TabId, deps: Partial<LogTabViewModelDependencies> = {}): LogTabViewModel => {
const selectedPod = new Pod(deploymentPod1);
const anotherPod = new Pod(deploymentPod2);
const selectedPod = deploymentPod1;
const anotherPod = deploymentPod2;
return mockLogTabViewModel(tabId, {
getLogTabData: () => ({
@@ -135,6 +132,10 @@ describe("<LogResourceSelector />", () => {
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(callForLogsInjectable, () => () => Promise.resolve("some-logs"));
di.override(ipcRendererInjectable, () => ({
on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
@@ -146,76 +147,90 @@ describe("<LogResourceSelector />", () => {
mockFs({
"tmp": {},
});
UserStore.createInstance();
ThemeStore.createInstance();
});
afterEach(() => {
UserStore.resetInstance();
ThemeStore.resetInstance();
mockFs.restore();
});
it("renders w/o errors", () => {
const model = getOnePodViewModel("foobar");
const { container } = render(<LogResourceSelector model={model} />);
describe("with one pod", () => {
let model: LogTabViewModel;
expect(container).toBeInstanceOf(HTMLElement);
beforeEach(() => {
model = getOnePodViewModel("foobar");
});
it("renders w/o errors", () => {
const { container } = render(<LogResourceSelector model={model} />);
expect(container).toBeInstanceOf(HTMLElement);
});
it("renders proper namespace", async () => {
const { findByTestId } = render(<LogResourceSelector model={model} />);
const ns = await findByTestId("namespace-badge");
expect(ns).toHaveTextContent("default");
});
it("renders proper selected items within dropdowns", async () => {
const { findByText } = render(<LogResourceSelector model={model} />);
expect(await findByText("dockerExporter")).toBeInTheDocument();
expect(await findByText("docker-exporter")).toBeInTheDocument();
});
});
it("renders proper namespace", async () => {
const model = getOnePodViewModel("foobar");
const { findByTestId } = render(<LogResourceSelector model={model} />);
const ns = await findByTestId("namespace-badge");
describe("with several pods", () => {
let model: LogTabViewModel;
let renameTab: jest.MockedFunction<LogTabViewModelDependencies["renameTab"]>;
expect(ns).toHaveTextContent("default");
});
beforeEach(() => {
renameTab = jest.fn();
model = getFewPodsTabData("foobar", { renameTab });
});
it("renders proper selected items within dropdowns", async () => {
const model = getOnePodViewModel("foobar");
const { findByText } = render(<LogResourceSelector model={model} />);
it("renders sibling pods in dropdown", async () => {
const { container, findByText } = render(<LogResourceSelector model={model} />);
const selector = container.querySelector<HTMLElement>(".pod-selector");
expect(await findByText("dockerExporter")).toBeInTheDocument();
expect(await findByText("docker-exporter")).toBeInTheDocument();
});
assert(selector);
it("renders sibling pods in dropdown", async () => {
const model = getFewPodsTabData("foobar");
const { container, findByText } = render(<LogResourceSelector model={model} />);
selectEvent.openMenu(selector);
expect(await findByText("deploymentPod2", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument();
expect(await findByText("deploymentPod1", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument();
});
selectEvent.openMenu(container.querySelector(".pod-selector"));
expect(await findByText("deploymentPod2", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument();
expect(await findByText("deploymentPod1", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument();
});
it("renders sibling containers in dropdown", async () => {
const { findByText, container } = render(<LogResourceSelector model={model} />);
const selector = container.querySelector<HTMLElement>(".container-selector");
it("renders sibling containers in dropdown", async () => {
const model = getFewPodsTabData("foobar");
const { findByText, container } = render(<LogResourceSelector model={model} />);
assert(selector);
selectEvent.openMenu(container.querySelector(".container-selector"));
expect(await findByText("node-exporter-1")).toBeInTheDocument();
expect(await findByText("init-node-exporter")).toBeInTheDocument();
expect(await findByText("init-node-exporter-1")).toBeInTheDocument();
});
selectEvent.openMenu(selector);
it("renders pod owner as badge", async () => {
const model = getFewPodsTabData("foobar");
const { findByText } = render(<LogResourceSelector model={model} />);
expect(await findByText("node-exporter-1")).toBeInTheDocument();
expect(await findByText("init-node-exporter")).toBeInTheDocument();
expect(await findByText("init-node-exporter-1")).toBeInTheDocument();
});
expect(await findByText("super-deployment", {
exact: false,
})).toBeInTheDocument();
});
it("renders pod owner as badge", async () => {
const { findByText } = render(<LogResourceSelector model={model} />);
it("updates tab name if selected pod changes", async () => {
const renameTab = jest.fn();
const model = getFewPodsTabData("foobar", { renameTab });
const { findByText, container } = render(<LogResourceSelector model={model} />);
expect(await findByText("super-deployment", {
exact: false,
})).toBeInTheDocument();
});
selectEvent.openMenu(container.querySelector(".pod-selector"));
it("updates tab name if selected pod changes", async () => {
const { findByText, container } = render(<LogResourceSelector model={model} />);
const selector = container.querySelector<HTMLElement>(".pod-selector");
userEvent.click(await findByText("deploymentPod2", { selector: ".pod-selector-menu .Select__option" }));
expect(renameTab).toBeCalledWith("foobar", "Pod deploymentPod2");
assert(selector);
selectEvent.openMenu(selector);
userEvent.click(await findByText("deploymentPod2", { selector: ".pod-selector-menu .Select__option" }));
expect(renameTab).toBeCalledWith("foobar", "Pod deploymentPod2");
});
});
});
@@ -5,7 +5,6 @@
import React from "react";
import { screen } from "@testing-library/react";
import { Pod } from "../../../../../common/k8s-api/endpoints";
import { dockerPod } from "./pod.mock";
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
import type { DiRender } from "../../../test-utils/renderFor";
@@ -37,7 +36,7 @@ function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependen
}
const getOnePodViewModel = (tabId: TabId, deps: Partial<LogTabViewModelDependencies> = {}): LogTabViewModel => {
const selectedPod = new Pod(dockerPod);
const selectedPod = dockerPod;
return mockLogTabViewModel(tabId, {
getLogTabData: () => ({
@@ -3,7 +3,9 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const dockerPod = {
import { Pod } from "../../../../../common/k8s-api/endpoints";
export const dockerPod = new Pod({
apiVersion: "v1",
kind: "dummy",
metadata: {
@@ -12,9 +14,10 @@ export const dockerPod = {
creationTimestamp: "dummy",
resourceVersion: "dummy",
namespace: "default",
selfLink: "/v1/pod/default/dockerExporter",
},
spec: {
initContainers: [] as any,
initContainers: [],
containers: [
{
name: "docker-exporter",
@@ -37,9 +40,9 @@ export const dockerPod = {
podIP: "dummy",
startTime: "dummy",
},
};
});
export const deploymentPod1 = {
export const deploymentPod1 = new Pod({
apiVersion: "v1",
kind: "dummy",
metadata: {
@@ -48,6 +51,7 @@ export const deploymentPod1 = {
creationTimestamp: "dummy",
resourceVersion: "dummy",
namespace: "default",
selfLink: "/v1/pod/default/deploymentPod1",
ownerReferences: [{
apiVersion: "v1",
kind: "Deployment",
@@ -97,9 +101,9 @@ export const deploymentPod1 = {
podIP: "dummy",
startTime: "dummy",
},
};
});
export const deploymentPod2 = {
export const deploymentPod2 = new Pod({
apiVersion: "v1",
kind: "dummy",
metadata: {
@@ -108,6 +112,7 @@ export const deploymentPod2 = {
creationTimestamp: "dummy",
resourceVersion: "dummy",
namespace: "default",
selfLink: "/v1/pod/default/deploymentPod2",
ownerReferences: [{
apiVersion: "v1",
kind: "Deployment",
@@ -157,9 +162,9 @@ export const deploymentPod2 = {
podIP: "dummy",
startTime: "dummy",
},
};
});
export const deploymentPod3 = {
export const deploymentPod3 = new Pod({
apiVersion: "v1",
kind: "dummy",
metadata: {
@@ -168,6 +173,7 @@ export const deploymentPod3 = {
creationTimestamp: "dummy",
resourceVersion: "dummy",
namespace: "default",
selfLink: "/v1/pod/default/deploymentPod3",
ownerReferences: [{
apiVersion: "v1",
kind: "Deployment",
@@ -205,4 +211,4 @@ export const deploymentPod3 = {
podIP: "dummy",
startTime: "dummy",
},
};
});
@@ -3,11 +3,19 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { podsApi } from "../../../../common/k8s-api/endpoints";
import type { PodLogsQuery } from "../../../../common/k8s-api/endpoints";
import podApiInjectable from "../../../../common/k8s-api/endpoints/pod.api.injectable";
import type { ResourceDescriptor } from "../../../../common/k8s-api/kube-api";
export type CallForLogs = (params: ResourceDescriptor, query?: PodLogsQuery) => Promise<string>;
const callForLogsInjectable = getInjectable({
id: "call-for-logs",
instantiate: () => podsApi.getLogs,
instantiate: (di): CallForLogs => {
const api = di.inject(podApiInjectable);
return (params, query) => api.getLogs(params, query);
},
});
export default callForLogsInjectable;
@@ -25,8 +25,9 @@ interface Dependencies {
const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependencies & LogControlsProps) => {
const tabData = model.logTabData.get();
const pod = model.pod.get();
if (!tabData) {
if (!tabData || !pod) {
return null;
}
@@ -44,7 +45,7 @@ const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependen
};
const downloadLogs = () => {
const fileName = model.pod.get().getName();
const fileName = pod.getName();
const logsToDownload: string[] = showTimestamps
? model.logs.get()
: model.logsWithoutTimestamps.get();
@@ -57,7 +58,8 @@ const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependen
<div className="time-range">
{since && (
<span>
Logs from{" "}
Logs from
{" "}
<b>{new Date(since).toLocaleString()}</b>
</span>
)}
@@ -3,13 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { IPodContainer, Pod } from "../../../../common/k8s-api/endpoints";
import type { PodContainer, Pod } from "../../../../common/k8s-api/endpoints";
import type { TabId } from "../dock/store";
import createLogsTabInjectable from "./create-logs-tab.injectable";
export interface PodLogsTabData {
selectedPod: Pod;
selectedContainer: IPodContainer;
selectedContainer: PodContainer;
}
const createPodLogsTabInjectable = getInjectable({
@@ -3,22 +3,27 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { podsStore } from "../../+workloads-pods/pods.store";
import type { WorkloadKubeObject } from "../../../../common/k8s-api/workload-kube-object";
import type { GetPodsByOwnerId } from "../../+workloads-pods/get-pods-by-owner-id.injectable";
import getPodsByOwnerIdInjectable from "../../+workloads-pods/get-pods-by-owner-id.injectable";
import type { DaemonSet, Deployment, Job, ReplicaSet, StatefulSet } from "../../../../common/k8s-api/endpoints";
import type { TabId } from "../dock/store";
import type { CreateLogsTabData } from "./create-logs-tab.injectable";
import createLogsTabInjectable from "./create-logs-tab.injectable";
export interface WorkloadLogsTabData {
workload: WorkloadKubeObject;
workload: StatefulSet | Job | Deployment | DaemonSet | ReplicaSet;
}
interface Dependencies {
createLogsTab: (title: string, data: CreateLogsTabData) => TabId;
getPodsByOwnerId: GetPodsByOwnerId;
}
const createWorkloadLogsTab = ({ createLogsTab }: Dependencies) => ({ workload }: WorkloadLogsTabData): TabId | undefined => {
const pods = podsStore.getPodsByOwnerId(workload.getId());
const createWorkloadLogsTab = ({
createLogsTab,
getPodsByOwnerId,
}: Dependencies) => ({ workload }: WorkloadLogsTabData): TabId | undefined => {
const pods = getPodsByOwnerId(workload.getId());
if (pods.length === 0) {
return undefined;
@@ -43,6 +48,7 @@ const createWorkloadLogsTabInjectable = getInjectable({
instantiate: (di) => createWorkloadLogsTab({
createLogsTab: di.inject(createLogsTabInjectable),
getPodsByOwnerId: di.inject(getPodsByOwnerIdInjectable),
}),
});
@@ -12,7 +12,7 @@ const getLogTabDataInjectable = getInjectable({
instantiate: (di) => {
const logTabStore = di.inject(logTabStoreInjectable);
return (tabId: string): LogTabData => logTabStore.getData(tabId);
return (tabId: string): LogTabData | undefined => logTabStore.getData(tabId);
},
});
+54 -21
View File
@@ -5,6 +5,7 @@
import "./list.scss";
import type { ForwardedRef } from "react";
import React from "react";
import AnsiUp from "ansi_up";
import DOMPurify from "dompurify";
@@ -16,6 +17,7 @@ import type { Align, ListOnScrollProps } from "react-window";
import { SearchStore } from "../../../search-store/search-store";
import { UserStore } from "../../../../common/user-store";
import { array, autoBind, cssNames } from "../../../utils";
import type { VirtualListRef } from "../../virtual-list";
import { VirtualList } from "../../virtual-list";
import { ToBottom } from "./to-bottom";
import type { LogTabViewModel } from "../logs/logs-view-model";
@@ -27,16 +29,20 @@ export interface LogListProps {
const colorConverter = new AnsiUp();
export interface LogListRef {
scrollToItem: (index: number, align: Align) => void;
}
@observer
export class LogList extends React.Component<LogListProps> {
class NonForwardedLogList extends React.Component<LogListProps & { innerRef: ForwardedRef<LogListRef> }> {
@observable isJumpButtonVisible = false;
@observable isLastLineVisible = true;
private virtualListDiv = React.createRef<HTMLDivElement>(); // A reference for outer container in VirtualList
private virtualListRef = React.createRef<VirtualList>(); // A reference for VirtualList component
private virtualListRef = React.createRef<VirtualListRef>(); // A reference for VirtualList component
private lineHeight = 18; // Height of a log line. Should correlate with styles in pod-log-list.scss
constructor(props: LogListProps) {
constructor(props: any) {
super(props);
makeObservable(this);
autoBind(this);
@@ -50,6 +56,27 @@ export class LogList extends React.Component<LogListProps> {
this.onUserScrolledUp(logs, prevLogs);
}),
]);
this.bindInnerRef({
scrollToItem: this.scrollToItem,
});
}
componentDidUpdate() {
this.bindInnerRef({
scrollToItem: this.scrollToItem,
});
}
componentWillUnmount() {
this.bindInnerRef(null);
}
private bindInnerRef(value: LogListRef | null) {
if (typeof this.props.innerRef === "function") {
this.props.innerRef(value);
} else if (this.props.innerRef && typeof this.props.innerRef === "object") {
this.props.innerRef.current = value;
}
}
onLogsInitialLoad(logs: string[], prevLogs: string[]) {
@@ -87,7 +114,7 @@ export class LogList extends React.Component<LogListProps> {
*/
@computed
get logs(): string[] {
const { showTimestamps } = this.props.model.logTabData.get();
const { showTimestamps } = this.props.model.logTabData.get() ?? {};
if (!showTimestamps) {
return this.props.model.logsWithoutTimestamps.get();
@@ -102,10 +129,8 @@ export class LogList extends React.Component<LogListProps> {
* Checks if JumpToBottom button should be visible and sets its observable
* @param props Scrolling props from virtual list core
*/
setButtonVisibility = action((props: ListOnScrollProps) => {
setButtonVisibility = action(({ scrollOffset }: ListOnScrollProps, { scrollHeight }: HTMLDivElement) => {
const offset = 100 * this.lineHeight;
const { scrollHeight } = this.virtualListDiv.current;
const { scrollOffset } = props;
if (scrollHeight - scrollOffset < offset) {
this.isJumpButtonVisible = false;
@@ -118,10 +143,7 @@ export class LogList extends React.Component<LogListProps> {
* Checks if last log line considered visible to user, setting its observable
* @param props Scrolling props from virtual list core
*/
setLastLineVisibility = action((props: ListOnScrollProps) => {
const { scrollHeight, clientHeight } = this.virtualListDiv.current;
const { scrollOffset } = props;
setLastLineVisibility = action(({ scrollOffset }: ListOnScrollProps, { scrollHeight, clientHeight }: HTMLDivElement) => {
this.isLastLineVisible = (clientHeight + scrollOffset) === scrollHeight;
});
@@ -143,7 +165,7 @@ export class LogList extends React.Component<LogListProps> {
};
scrollToItem = (index: number, align: Align) => {
this.virtualListRef.current.scrollToItem(index, align);
this.virtualListRef.current?.scrollToItem(index, align);
};
onScroll = (props: ListOnScrollProps) => {
@@ -152,10 +174,13 @@ export class LogList extends React.Component<LogListProps> {
};
onScrollDebounced = debounce((props: ListOnScrollProps) => {
if (!this.virtualListDiv.current) return;
this.setButtonVisibility(props);
this.setLastLineVisibility(props);
this.checkLoadIntent(props);
const virtualList = this.virtualListDiv.current;
if (virtualList) {
this.setButtonVisibility(props, virtualList);
this.setLastLineVisibility(props, virtualList);
this.checkLoadIntent(props);
}
}, 700); // Increasing performance and giving some time for virtual list to settle down
/**
@@ -182,10 +207,12 @@ export class LogList extends React.Component<LogListProps> {
const lastItem = index === pieces.length - 1;
const overlayValue = matches.next().value;
const overlay = !lastItem
? <span
className={cssNames("overlay", { active })}
dangerouslySetInnerHTML={{ __html: ansiToHtml(overlayValue) }}
/>
? (
<span
className={cssNames("overlay", { active })}
dangerouslySetInnerHTML={{ __html: ansiToHtml(overlayValue) }}
/>
)
: null;
contents.push(
@@ -220,7 +247,9 @@ export class LogList extends React.Component<LogListProps> {
if (!this.logs.length) {
return (
<div className="LogList flex box grow align-center justify-center">
There are no logs available for container {this.props.model.logTabData.get()?.selectedContainer}
There are no logs available for container
{" "}
{this.props.model.logTabData.get()?.selectedContainer}
</div>
);
}
@@ -243,3 +272,7 @@ export class LogList extends React.Component<LogListProps> {
);
}
}
export const LogList = React.forwardRef<LogListRef, LogListProps>((props, ref) => (
<NonForwardedLogList {...props} innerRef={ref} />
));
@@ -8,17 +8,21 @@ import type { Pod } from "../../../../common/k8s-api/endpoints";
import logStoreInjectable from "./store.injectable";
import type { LogTabData } from "./tab-store";
export interface LoadLogs {
(
tabId: string,
pod: IComputedValue<Pod | undefined>,
logTabData: IComputedValue<LogTabData | undefined>,
): Promise<void>;
}
const loadLogsInjectable = getInjectable({
id: "load-logs",
instantiate: (di) => {
instantiate: (di): LoadLogs => {
const logStore = di.inject(logStoreInjectable);
return (
tabId: string,
pod: IComputedValue<Pod | undefined>,
logTabData: IComputedValue<LogTabData>,
): Promise<void> => logStore.load(tabId, pod, logTabData);
return (tabId, pod, logTabData) => logStore.load(tabId, pod, logTabData);
},
});
@@ -13,10 +13,11 @@ import getLogTabDataInjectable from "./get-log-tab-data.injectable";
import loadLogsInjectable from "./load-logs.injectable";
import setLogTabDataInjectable from "./set-log-tab-data.injectable";
import stopLoadingLogsInjectable from "./stop-loading-logs.injectable";
import { podsStore } from "../../+workloads-pods/pods.store";
import renameTabInjectable from "../dock/rename-tab.injectable";
import areLogsPresentInjectable from "./are-logs-present.injectable";
import searchStoreInjectable from "../../../search-store/search-store.injectable";
import getPodsByOwnerIdInjectable from "../../+workloads-pods/get-pods-by-owner-id.injectable";
import getPodByIdInjectable from "../../+workloads-pods/get-pod-by-id.injectable";
export interface InstantiateArgs {
tabId: TabId;
@@ -36,8 +37,8 @@ const logsViewModelInjectable = getInjectable({
renameTab: di.inject(renameTabInjectable),
stopLoadingLogs: di.inject(stopLoadingLogsInjectable),
areLogsPresent: di.inject(areLogsPresentInjectable),
getPodById: id => podsStore.getById(id),
getPodsByOwnerId: id => podsStore.getPodsByOwnerId(id),
getPodById: di.inject(getPodByIdInjectable),
getPodsByOwnerId: di.inject(getPodsByOwnerIdInjectable),
searchStore: di.inject(searchStoreInjectable),
}),
lifecycle: lifecycleEnum.transient,
@@ -8,19 +8,24 @@ import { computed } from "mobx";
import type { TabId } from "../dock/store";
import type { SearchStore } from "../../../search-store/search-store";
import type { Pod } from "../../../../common/k8s-api/endpoints";
import { isDefined } from "../../../utils";
import assert from "assert";
import type { GetPodById } from "../../+workloads-pods/get-pod-by-id.injectable";
import type { GetPodsByOwnerId } from "../../+workloads-pods/get-pods-by-owner-id.injectable";
import type { LoadLogs } from "./load-logs.injectable";
export interface LogTabViewModelDependencies {
getLogs: (tabId: TabId) => string[];
getLogsWithoutTimestamps: (tabId: TabId) => string[];
getTimestampSplitLogs: (tabId: TabId) => [string, string][];
getLogTabData: (tabId: TabId) => LogTabData;
getLogTabData: (tabId: TabId) => LogTabData | undefined;
setLogTabData: (tabId: TabId, data: LogTabData) => void;
loadLogs: (tabId: TabId, pod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>) => Promise<void>;
reloadLogs: (tabId: TabId, pod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>) => Promise<void>;
loadLogs: LoadLogs;
reloadLogs: (tabId: TabId, pod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData | undefined>) => Promise<void>;
renameTab: (tabId: TabId, title: string) => void;
stopLoadingLogs: (tabId: TabId) => void;
getPodById: (id: string) => Pod | undefined;
getPodsByOwnerId: (id: string) => Pod[];
getPodById: GetPodById;
getPodsByOwnerId: GetPodsByOwnerId;
areLogsPresent: (tabId: TabId) => boolean;
searchStore: SearchStore;
}
@@ -48,7 +53,7 @@ export class LogTabViewModel {
return this.dependencies.getPodsByOwnerId(data.owner.uid);
}
return [this.dependencies.getPodById(data.selectedPodId)];
return [this.dependencies.getPodById(data.selectedPodId)].filter(isDefined);
});
readonly pod = computed(() => {
const data = this.logTabData.get();
@@ -61,7 +66,11 @@ export class LogTabViewModel {
});
updateLogTabData = (partialData: Partial<LogTabData>) => {
this.dependencies.setLogTabData(this.tabId, { ...this.logTabData.get(), ...partialData });
const data = this.logTabData.get();
assert(data, "Can only update data once it is set");
this.dependencies.setLogTabData(this.tabId, { ...data, ...partialData });
};
loadLogs = () => this.dependencies.loadLogs(this.tabId, this.pod, this.logTabData);
@@ -17,7 +17,7 @@ const reloadLogsInjectable = getInjectable({
return (
tabId: string,
pod: IComputedValue<Pod | undefined>,
logTabData: IComputedValue<LogTabData>,
logTabData: IComputedValue<LogTabData | undefined>,
): Promise<void> => logStore.reload(tabId, pod, logTabData);
},
});
@@ -12,19 +12,13 @@ import { Badge } from "../../badge";
import type { SelectOption } from "../../select";
import { Select } from "../../select";
import type { LogTabViewModel } from "./logs-view-model";
import type { IPodContainer, Pod } from "../../../../common/k8s-api/endpoints";
import type { PodContainer, Pod } from "../../../../common/k8s-api/endpoints";
import type { SingleValue } from "react-select";
export interface LogResourceSelectorProps {
model: LogTabViewModel;
}
function getSelectOptions(containers: IPodContainer[]): SelectOption<string>[] {
return containers.map(container => ({
value: container.name,
label: container.name,
}));
}
export const LogResourceSelector = observer(({ model }: LogResourceSelectorProps) => {
const tabData = model.logTabData.get();
@@ -40,68 +34,85 @@ export const LogResourceSelector = observer(({ model }: LogResourceSelectorProps
return null;
}
const onContainerChange = (option: SelectOption<string>) => {
const podOptions = pods.map(pod => ({
value: pod,
label: pod.getName(),
}));
const allContainers = pod.getAllContainers();
const container = allContainers.find(container => container.name === selectedContainer) ?? null;
const onContainerChange = (option: SingleValue<SelectOption<PodContainer>>) => {
if (!option) {
return;
}
model.updateLogTabData({
selectedContainer: option.value,
selectedContainer: option.value.name,
});
model.reloadLogs();
};
const onPodChange = ({ value }: SelectOption<Pod>) => {
const onPodChange = (option: SingleValue<SelectOption<Pod>>) => {
if (!option) {
return;
}
model.updateLogTabData({
selectedPodId: value.getId(),
selectedContainer: value.getAllContainers()[0]?.name,
selectedPodId: option.value.getId(),
selectedContainer: option.value.getAllContainers()[0]?.name,
});
model.renameTab(`Pod ${value.getName()}`);
model.renameTab(`Pod ${option.value.getName()}`);
model.reloadLogs();
};
const containerSelectOptions = [
{
label: "Containers",
options: getSelectOptions(pod.getContainers()),
options: pod.getContainers().map(container => ({
value: container,
label: container.name,
})),
},
{
label: "Init Containers",
options: getSelectOptions(pod.getInitContainers()),
options: pod.getInitContainers().map(container => ({
value: container,
label: container.name,
})),
},
];
const podSelectOptions = pods.map(pod => ({
label: pod.getName(),
value: pod,
}));
return (
<div className="LogResourceSelector flex gaps align-center">
<span>Namespace</span> <Badge data-testid="namespace-badge" label={pod.getNs()}/>
<span>Namespace</span>
{" "}
<Badge data-testid="namespace-badge" label={pod.getNs()} />
{
owner && (
<>
<span>Owner</span> <Badge data-testid="namespace-badge" label={`${owner.kind} ${owner.name}`}/>
<span>Owner</span>
{" "}
<Badge data-testid="namespace-badge" label={`${owner.kind} ${owner.name}`} />
</>
)
}
<span>Pod</span>
<Select
id="pod-selection-input"
options={podSelectOptions}
value={podSelectOptions.find(opt => opt.value === pod)}
formatOptionLabel={option => option.label}
options={podOptions}
value={pod}
isClearable={false}
onChange={onPodChange}
autoConvertOptions={false}
className="pod-selector"
menuClass="pod-selector-menu"
/>
<span>Container</span>
<Select
<Select<PodContainer, SelectOption<PodContainer>, false>
id="container-selector-input"
options={containerSelectOptions}
value={{ label: selectedContainer, value: selectedContainer }}
value={container}
onChange={onContainerChange}
autoConvertOptions={false}
className="container-selector"
menuClass="container-selector-menu"
controlShouldRenderValue
/>
</div>
);
+1 -1
View File
@@ -73,7 +73,7 @@ export const LogSearch = observer(({ onSearch, scrollToOverlay, model: { logTabD
showClearIcon={true}
contentRight={totalFinds > 0 && (
<div className="find-count">
{activeFind}/{totalFinds}
{`${activeFind} / ${totalFinds}`}
</div>
)}
onClear={onClear}
+31 -16
View File
@@ -4,11 +4,13 @@
*/
import type { IComputedValue } from "mobx";
import { observable, when } from "mobx";
import type { IPodLogsQuery, Pod } from "../../../../common/k8s-api/endpoints";
import { observable } from "mobx";
import type { PodLogsQuery, Pod } from "../../../../common/k8s-api/endpoints";
import { waitUntilDefined } from "../../../../common/utils/wait";
import type { IntervalFn } from "../../../utils";
import { getOrInsertWith, interval } from "../../../utils";
import type { TabId } from "../dock/store";
import type { CallForLogs } from "./call-for-logs.injectable";
import type { LogTabData } from "./tab-store";
type PodLogLine = string;
@@ -16,7 +18,7 @@ type PodLogLine = string;
const logLinesToLoad = 500;
interface Dependencies {
callForLogs: ({ namespace, name }: { namespace: string; name: string }, query: IPodLogsQuery) => Promise<string>;
callForLogs: CallForLogs;
}
export class LogStore {
@@ -45,7 +47,7 @@ export class LogStore {
* Also, it handles loading errors, rewriting whole logs with error
* messages
*/
public async load(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): Promise<void> {
public async load(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData | undefined>): Promise<void> {
try {
const logs = await this.loadLogs(computedPod, logTabData, {
tailLines: this.getLogLines(tabId) + logLinesToLoad,
@@ -58,7 +60,7 @@ export class LogStore {
}
}
private getRefresher(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): IntervalFn {
private getRefresher(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData | undefined>): IntervalFn {
return getOrInsertWith(this.refreshers, tabId, () => (
interval(10, () => {
if (this.podLogs.has(tabId)) {
@@ -82,13 +84,14 @@ export class LogStore {
* starting from last line received.
* @param tabId
*/
public async loadMore(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): Promise<void> {
if (!this.podLogs.get(tabId).length) {
public async loadMore(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData | undefined>): Promise<void> {
const oldLogs = this.podLogs.get(tabId);
if (!oldLogs?.length) {
return;
}
try {
const oldLogs = this.podLogs.get(tabId);
const logs = await this.loadLogs(computedPod, logTabData, {
sinceTime: this.getLastSinceTime(tabId),
});
@@ -107,11 +110,23 @@ export class LogStore {
* @param params request parameters described in IPodLogsQuery interface
* @returns A fetch request promise
*/
private async loadLogs(computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>, params: Partial<IPodLogsQuery>): Promise<string[]> {
await when(() => Boolean(computedPod.get() && logTabData.get()), { timeout: 5_000 });
private async loadLogs(computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData | undefined>, params: Partial<PodLogsQuery>): Promise<string[]> {
const {
pod,
tabData: {
selectedContainer,
showPrevious,
},
} = await waitUntilDefined(() => {
const pod = computedPod.get();
const tabData = logTabData.get();
const { selectedContainer, showPrevious } = logTabData.get();
const pod = computedPod.get();
if (pod && tabData) {
return { pod, tabData };
}
return undefined;
});
const namespace = pod.getNs();
const name = pod.getName();
@@ -177,9 +192,9 @@ export class LogStore {
* @param tabId
*/
getLastSinceTime(tabId: TabId): string {
const logs = this.podLogs.get(tabId);
const logs = this.podLogs.get(tabId) ?? [];
const timestamps = this.getTimestamps(logs[logs.length - 1]);
const stamp = new Date(timestamps ? timestamps[0] : null);
const stamp = timestamps[0] ? new Date(timestamps[0]) : new Date();
stamp.setSeconds(stamp.getSeconds() + 1); // avoid duplicates from last second
@@ -197,7 +212,7 @@ export class LogStore {
}
getTimestamps(logs: string): RegExpMatchArray {
return logs.match(/^\d+\S+/gm);
return logs.match(/^\d+\S+/gm) ?? [];
}
removeTimestamps(logs: string): string {
@@ -208,7 +223,7 @@ export class LogStore {
this.podLogs.delete(tabId);
}
reload(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): Promise<void> {
reload(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData | undefined>): Promise<void> {
this.clearLogs(tabId);
return this.load(tabId, computedPod, logTabData);
@@ -3,11 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DockTabStorageState } from "../dock-tab-store/dock-tab.store";
import { DockTabStore } from "../dock-tab-store/dock-tab.store";
import type { StorageHelper } from "../../../utils";
import type { TabId } from "../dock/store";
import { logTabDataValidator } from "./log-tab-data.validator";
import type { CreateStorage } from "../../../utils/create-storage/create-storage";
export interface LogTabOwnerRef {
/**
@@ -58,7 +57,7 @@ export interface LogTabData {
}
interface Dependencies {
createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>;
createStorage: CreateStorage;
}
export class LogTabStore extends DockTabStore<LogTabData> {
+17 -10
View File
@@ -7,6 +7,7 @@ import React, { createRef, useEffect } from "react";
import { observer } from "mobx-react";
import { InfoPanel } from "../info-panel";
import { LogResourceSelector } from "./resource-selector";
import type { LogListRef } from "./list";
import { LogList } from "./list";
import { LogSearch } from "./search";
import { LogControls } from "./controls";
@@ -14,13 +15,11 @@ import { withInjectables } from "@ogre-tools/injectable-react";
import logsViewModelInjectable from "./logs-view-model.injectable";
import type { LogTabViewModel } from "./logs-view-model";
import type { DockTab } from "../dock/store";
import type { Disposer } from "../../../utils";
import { cssNames } from "../../../utils";
import type { KubeWatchSubscribeStoreOptions } from "../../../kube-watch-api/kube-watch-api";
import type { SubscribeStores } from "../../../kube-watch-api/kube-watch-api";
import subscribeStoresInjectable from "../../../kube-watch-api/subscribe-stores.injectable";
import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
import { podsStore } from "../../+workloads-pods/pods.store";
import type { PodStore } from "../../+workloads-pods/store";
import podStoreInjectable from "../../+workloads-pods/store.injectable";
export interface LogsDockTabProps {
className?: string;
@@ -29,11 +28,18 @@ export interface LogsDockTabProps {
interface Dependencies {
model: LogTabViewModel;
subscribeStores: (stores: KubeObjectStore<KubeObject>[], opts?: KubeWatchSubscribeStoreOptions) => Disposer;
subscribeStores: SubscribeStores;
podStore: PodStore;
}
const NonInjectedLogsDockTab = observer(({ className, tab, model, subscribeStores }: Dependencies & LogsDockTabProps) => {
const logListElement = createRef<LogList>();
const NonInjectedLogsDockTab = observer(({
className,
tab,
model,
subscribeStores,
podStore,
}: Dependencies & LogsDockTabProps) => {
const logListElement = createRef<LogListRef>();
const data = model.logTabData.get();
useEffect(() => {
@@ -42,7 +48,7 @@ const NonInjectedLogsDockTab = observer(({ className, tab, model, subscribeStore
return model.stopLoadingLogs;
}, [tab.id]);
useEffect(() => subscribeStores([
podsStore,
podStore,
], {
namespaces: data ? [data.namespace] : [],
}), [data?.namespace]);
@@ -94,10 +100,11 @@ const NonInjectedLogsDockTab = observer(({ className, tab, model, subscribeStore
export const LogsDockTab = withInjectables<Dependencies, LogsDockTabProps>(NonInjectedLogsDockTab, {
getProps: (di, props) => ({
...props,
model: di.inject(logsViewModelInjectable, {
tabId: props.tab.id,
}),
subscribeStores: di.inject(subscribeStoresInjectable),
...props,
podStore: di.inject(podStoreInjectable),
}),
});
@@ -3,13 +3,29 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { TerminalDependencies } from "./terminal";
import { Terminal } from "./terminal";
import type { TabId } from "../dock/store";
import type { TerminalApi } from "../../../api/terminal-api";
import terminalSpawningPoolInjectable from "./terminal-spawning-pool.injectable";
import terminalConfigInjectable from "../../../../common/user-store/terminal-config.injectable";
import terminalCopyOnSelectInjectable from "../../../../common/user-store/terminal-copy-on-select.injectable";
import themeStoreInjectable from "../../../themes/store.injectable";
export type CreateTerminal = (tabId: TabId, api: TerminalApi) => Terminal;
const createTerminalInjectable = getInjectable({
id: "create-terminal",
instantiate: () => (tabId: TabId, api: TerminalApi) => new Terminal(tabId, api),
instantiate: (di): CreateTerminal => {
const dependencies: TerminalDependencies = {
spawningPool: di.inject(terminalSpawningPoolInjectable),
terminalConfig: di.inject(terminalConfigInjectable),
terminalCopyOnSelect: di.inject(terminalCopyOnSelectInjectable),
themeStore: di.inject(themeStoreInjectable),
};
return (tabId, api) => new Terminal(dependencies, { tabId, api });
},
});
export default createTerminalInjectable;
@@ -34,22 +34,36 @@ class NonInjectedTerminalTab<Props extends TerminalTabProps & Dependencies> exte
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => this.isDisconnected === true, () => {
this.props.dockStore.closeTab(this.tabId);
}),
reaction(() => this.isDisconnected, this.close),
]);
}
get tabId() {
return this.props.value.id;
private close() {
const { tabId } = this;
if (tabId) {
this.props.dockStore.closeTab(tabId);
}
}
get isDisconnected() {
return this.props.terminalStore.isDisconnected(this.tabId);
private get tabId() {
return this.props.value?.id;
}
reconnect() {
this.props.terminalStore.reconnect(this.tabId);
private get isDisconnected() {
const { tabId } = this;
return tabId
? this.props.terminalStore.isDisconnected(tabId)
: false;
}
private reconnect() {
const { tabId } = this;
if (tabId) {
this.props.terminalStore.reconnect(tabId);
}
}
render() {
@@ -3,7 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { TerminalApi } from "../../../api/terminal-api";
import type { TabId } from "../dock/store";
import terminalStoreInjectable from "./store.injectable";
@@ -13,7 +12,7 @@ const getTerminalApiInjectable = getInjectable({
instantiate: (di) => {
const terminalStore = di.inject(terminalStoreInjectable);
return (tabId: TabId): TerminalApi => terminalStore.getTerminalApi(tabId);
return (tabId: TabId) => terminalStore.getTerminalApi(tabId);
},
});
@@ -4,6 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { when } from "mobx";
import { waitUntilDefined } from "../../../../common/utils/wait";
import type { TerminalApi } from "../../../api/terminal-api";
import { TerminalChannels } from "../../../api/terminal-api";
import { noop } from "../../../utils";
@@ -16,7 +17,7 @@ import getTerminalApiInjectable from "./get-terminal-api.injectable";
interface Dependencies {
selectTab: (tabId: TabId) => void;
createTerminalTab: () => DockTab;
getTerminalApi: (tabId: TabId) => TerminalApi;
getTerminalApi: (tabId: TabId) => TerminalApi | undefined;
}
export interface SendCommandOptions {
@@ -37,7 +38,7 @@ export interface SendCommandOptions {
}
const sendCommand = ({ selectTab, createTerminalTab, getTerminalApi }: Dependencies) => async (command: string, options: SendCommandOptions = {}): Promise<void> => {
let { tabId } = options;
let tabId: string | undefined = options.tabId;
if (tabId) {
selectTab(tabId);
@@ -45,10 +46,12 @@ const sendCommand = ({ selectTab, createTerminalTab, getTerminalApi }: Dependenc
tabId = createTerminalTab().id;
}
await when(() => Boolean(getTerminalApi(tabId)));
const terminalApi = getTerminalApi(tabId);
const shellIsReady = when(() =>terminalApi.isReady);
const terminalApi = await waitUntilDefined(() => (
tabId
? getTerminalApi(tabId)
: undefined
));
const shellIsReady = when(() => terminalApi.isReady);
const notifyVeryLong = setTimeout(() => {
shellIsReady.cancel();
Notifications.info(
@@ -5,12 +5,14 @@
import { getInjectable } from "@ogre-tools/injectable";
import { TerminalStore } from "./store";
import createTerminalInjectable from "./create-terminal.injectable";
import createTerminalApiInjectable from "../../../api/create-terminal-api.injectable";
const terminalStoreInjectable = getInjectable({
id: "terminal-store",
instantiate: (di) => new TerminalStore({
createTerminal: di.inject(createTerminalInjectable),
createTerminalApi: di.inject(createTerminalApiInjectable),
}),
});
@@ -5,16 +5,19 @@
import { action, observable } from "mobx";
import type { Terminal } from "./terminal";
import { TerminalApi } from "../../../api/terminal-api";
import type { TerminalApi } from "../../../api/terminal-api";
import type { DockTab, TabId } from "../dock/store";
import { WebSocketApiState } from "../../../api/websocket-api";
import type { CreateTerminalApi } from "../../../api/create-terminal-api.injectable";
import type { CreateTerminal } from "./create-terminal.injectable";
export interface ITerminalTab extends DockTab {
node?: string; // activate node shell mode
}
interface Dependencies {
createTerminal: (tabId: TabId, api: TerminalApi) => Terminal;
createTerminal: CreateTerminal;
createTerminalApi: CreateTerminalApi;
}
export class TerminalStore {
@@ -29,7 +32,7 @@ export class TerminalStore {
if (this.isConnected(tab.id)) {
return;
}
const api = new TerminalApi({
const api = this.dependencies.createTerminalApi({
id: tab.id,
node: tab.node,
});
@@ -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 assert from "assert";
const terminalSpawningPoolInjectable = getInjectable({
id: "terminal-spawning-pool",
instantiate: () => {
const pool = document.getElementById("terminal-init");
assert(pool, "DOM MUST contain #terminal-init element");
return pool;
},
causesSideEffects: true,
});
export default terminalSpawningPoolInjectable;
@@ -4,47 +4,54 @@
*/
import debounce from "lodash/debounce";
import type { IComputedValue } from "mobx";
import { reaction } from "mobx";
import { Terminal as XTerm } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import type { TabId } from "../dock/store";
import type { TerminalApi } from "../../../api/terminal-api";
import { TerminalChannels } from "../../../api/terminal-api";
import { ThemeStore } from "../../../theme.store";
import type { ThemeStore } from "../../../themes/store";
import { disposer } from "../../../utils";
import { isMac } from "../../../../common/vars";
import { once } from "lodash";
import { UserStore } from "../../../../common/user-store";
import { clipboard } from "electron";
import logger from "../../../../common/logger";
import type { TerminalConfig } from "../../../../common/user-store/preferences-helpers";
import assert from "assert";
export interface TerminalDependencies {
readonly spawningPool: HTMLElement;
readonly terminalConfig: IComputedValue<TerminalConfig>;
readonly terminalCopyOnSelect: IComputedValue<boolean>;
readonly themeStore: ThemeStore;
}
export interface TerminalArguments {
tabId: TabId;
api: TerminalApi;
}
export class Terminal {
private terminalConfig: TerminalConfig = UserStore.getInstance().terminalConfig;
public static get spawningPool() {
return document.getElementById("terminal-init");
}
private xterm: XTerm | null = new XTerm({
cursorBlink: true,
cursorStyle: "bar",
fontSize: this.terminalConfig.fontSize,
fontFamily: this.terminalConfig.fontFamily,
});
private readonly xterm: XTerm;
private readonly fitAddon = new FitAddon();
private scrollPos = 0;
private disposer = disposer();
private readonly disposer = disposer();
public readonly tabId: TabId;
protected readonly api: TerminalApi;
get elem() {
return this.xterm?.element;
private get elem() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.xterm.element!;
}
get viewport() {
return this.xterm.element.querySelector(".xterm-viewport");
private get viewport() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.elem.querySelector(".xterm-viewport")!;
}
attachTo(parentElem: HTMLElement) {
assert(this.elem, "Terminal should always be mounted somewhere");
parentElem.appendChild(this.elem);
this.onActivate();
}
@@ -53,15 +60,25 @@ export class Terminal {
const { elem } = this;
if (elem) {
Terminal.spawningPool.appendChild(elem);
this.dependencies.spawningPool.appendChild(elem);
}
}
constructor(public tabId: TabId, protected api: TerminalApi) {
constructor(protected readonly dependencies: TerminalDependencies, { tabId, api }: TerminalArguments) {
this.tabId = tabId;
this.api = api;
const { fontSize, fontFamily } = this.dependencies.terminalConfig.get();
this.xterm = new XTerm({
cursorBlink: true,
cursorStyle: "bar",
fontSize,
fontFamily,
});
// enable terminal addons
this.xterm.loadAddon(this.fitAddon);
this.xterm.open(Terminal.spawningPool);
this.xterm.open(this.dependencies.spawningPool);
this.xterm.registerLinkMatcher(/https?:\/\/[^\s]+/i, this.onClickLink);
this.xterm.attachCustomKeyEventHandler(this.keyHandler);
this.xterm.onSelectionChange(this.onSelectionChange);
@@ -78,15 +95,15 @@ export class Terminal {
window.addEventListener("resize", this.onResize);
this.disposer.push(
reaction(() => ThemeStore.getInstance().xtermColors, colors => {
reaction(() => this.dependencies.themeStore.xtermColors, colors => {
this.xterm?.setOption("theme", colors);
}, {
fireImmediately: true,
}),
reaction(() => UserStore.getInstance().terminalConfig.fontSize, this.setFontSize, {
reaction(() => this.dependencies.terminalConfig.get().fontSize, this.setFontSize, {
fireImmediately: true,
}),
reaction(() => UserStore.getInstance().terminalConfig.fontFamily, this.setFontFamily, {
reaction(() => this.dependencies.terminalConfig.get().fontFamily, this.setFontFamily, {
fireImmediately: true,
}),
() => onDataHandler.dispose(),
@@ -98,11 +115,8 @@ export class Terminal {
}
destroy() {
if (this.xterm) {
this.disposer();
this.xterm.dispose();
this.xterm = null;
}
this.disposer();
this.xterm.dispose();
}
fit = () => {
@@ -166,7 +180,7 @@ export class Terminal {
onContextMenu = () => {
if (
// don't paste if user hasn't turned on the feature
UserStore.getInstance().terminalCopyOnSelect
this.dependencies.terminalCopyOnSelect.get()
// don't paste if the clipboard doesn't have text
&& clipboard.availableFormats().includes("text/plain")
@@ -178,7 +192,7 @@ export class Terminal {
onSelectionChange = () => {
const selection = this.xterm.getSelection().trim();
if (UserStore.getInstance().terminalCopyOnSelect && selection) {
if (this.dependencies.terminalCopyOnSelect.get() && selection) {
clipboard.writeText(selection);
}
};
+21 -9
View File
@@ -10,11 +10,13 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { cssNames } from "../../../utils";
import type { Terminal } from "./terminal";
import type { TerminalStore } from "./store";
import { ThemeStore } from "../../../theme.store";
import type { ThemeStore } from "../../../themes/store";
import type { DockTab, DockStore } from "../dock/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "../dock/store.injectable";
import terminalStoreInjectable from "./store.injectable";
import assert from "assert";
import themeStoreInjectable from "../../../themes/store.injectable";
export interface TerminalWindowProps {
tab: DockTab;
@@ -23,17 +25,22 @@ export interface TerminalWindowProps {
interface Dependencies {
dockStore: DockStore;
terminalStore: TerminalStore;
themeStore: ThemeStore;
}
@observer
class NonInjectedTerminalWindow extends React.Component<TerminalWindowProps & Dependencies> {
public elem: HTMLElement;
public terminal: Terminal;
public elem: HTMLElement | null = null;
public terminal!: Terminal;
componentDidMount() {
this.props.terminalStore.connect(this.props.tab);
this.terminal = this.props.terminalStore.getTerminal(this.props.tab.id);
this.terminal.attachTo(this.elem);
const terminal = this.props.terminalStore.getTerminal(this.props.tab.id);
assert(terminal, "Terminal must be created for tab before mounting");
this.terminal = terminal;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.terminal.attachTo(this.elem!);
disposeOnUnmount(this, [
// refresh terminal available space (cols/rows) when <Dock/> resized
@@ -46,8 +53,12 @@ class NonInjectedTerminalWindow extends React.Component<TerminalWindowProps & De
componentDidUpdate(): void {
this.terminal.detach();
this.props.terminalStore.connect(this.props.tab);
this.terminal = this.props.terminalStore.getTerminal(this.props.tab.id);
this.terminal.attachTo(this.elem);
const terminal = this.props.terminalStore.getTerminal(this.props.tab.id);
assert(terminal, "Terminal must be created for tab before mounting");
this.terminal = terminal;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.terminal.attachTo(this.elem!);
}
componentWillUnmount(): void {
@@ -57,7 +68,7 @@ class NonInjectedTerminalWindow extends React.Component<TerminalWindowProps & De
render() {
return (
<div
className={cssNames("TerminalWindow", ThemeStore.getInstance().activeTheme.type)}
className={cssNames("TerminalWindow", this.props.themeStore.activeTheme.type)}
ref={elem => this.elem = elem}
/>
);
@@ -66,9 +77,10 @@ class NonInjectedTerminalWindow extends React.Component<TerminalWindowProps & De
export const TerminalWindow = withInjectables<Dependencies, TerminalWindowProps>(NonInjectedTerminalWindow, {
getProps: (di, props) => ({
...props,
dockStore: di.inject(dockStoreInjectable),
terminalStore: di.inject(terminalStoreInjectable),
...props,
themeStore: di.inject(themeStoreInjectable),
}),
});
@@ -5,19 +5,18 @@
import { action, computed, makeObservable } from "mobx";
import type { TabId } from "../dock/store";
import type { DockTabStorageState } from "../dock-tab-store/dock-tab.store";
import type { DockTabStoreDependencies } from "../dock-tab-store/dock-tab.store";
import { DockTabStore } from "../dock-tab-store/dock-tab.store";
import { getReleaseValues } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import type { StorageHelper } from "../../../utils";
import assert from "assert";
export interface IChartUpgradeData {
releaseName: string;
releaseNamespace: string;
}
interface Dependencies {
export interface UpgradeChartTabStoreDependencies extends DockTabStoreDependencies {
valuesStore: DockTabStore<string>;
createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>;
}
export class UpgradeChartTabStore extends DockTabStore<IChartUpgradeData> {
@@ -29,7 +28,7 @@ export class UpgradeChartTabStore extends DockTabStore<IChartUpgradeData> {
return this.dependencies.valuesStore;
}
constructor(protected dependencies : Dependencies) {
constructor(protected readonly dependencies: UpgradeChartTabStoreDependencies) {
super(dependencies, {
storageKey: "chart_releases",
});
@@ -40,13 +39,17 @@ export class UpgradeChartTabStore extends DockTabStore<IChartUpgradeData> {
@action
async reloadValues(tabId: TabId) {
this.values.clearData(tabId); // reset
const { releaseName, releaseNamespace } = this.getData(tabId);
const data = this.getData(tabId);
assert(data, "cannot reload values if no data");
const { releaseName, releaseNamespace } = data;
const values = await getReleaseValues(releaseName, releaseNamespace, true);
this.values.setData(tabId, values);
}
getTabIdByRelease(releaseName: string): TabId {
getTabIdByRelease(releaseName: string) {
return this.releaseNameReverseLookup.get(releaseName);
}
}
@@ -15,8 +15,8 @@ import type { UpgradeChartTabStore } from "./store";
import { Spinner } from "../../spinner";
import { Badge } from "../../badge";
import { EditorPanel } from "../editor-panel";
import { helmChartStore, type IChartVersion } from "../../+helm-charts/helm-chart.store";
import type { HelmRelease, IReleaseUpdateDetails, IReleaseUpdatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import { helmChartStore, type ChartVersion } from "../../+helm-charts/helm-chart.store";
import type { HelmRelease, HelmReleaseUpdateDetails, HelmReleaseUpdatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import type { SelectOption } from "../../select";
import { Select } from "../../select";
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
@@ -33,14 +33,14 @@ export interface UpgradeChartProps {
interface Dependencies {
releases: IAsyncComputed<HelmRelease[]>;
upgradeChartTabStore: UpgradeChartTabStore;
updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise<IReleaseUpdateDetails>;
updateRelease: (name: string, namespace: string, payload: HelmReleaseUpdatePayload) => Promise<HelmReleaseUpdateDetails>;
}
@observer
export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps & Dependencies> {
@observable error: string;
@observable versions = observable.array<IChartVersion>();
@observable version: IChartVersion;
@observable error?: string;
@observable versions = observable.array<ChartVersion>();
@observable version: ChartVersion | undefined = undefined;
constructor(props: UpgradeChartProps & Dependencies) {
super(props);
@@ -70,7 +70,7 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
return this.props.tab.id;
}
get release(): HelmRelease {
get release() {
const tabData = this.props.upgradeChartTabStore.getData(this.tabId);
if (!tabData) return null;
@@ -86,14 +86,14 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
this.props.upgradeChartTabStore.reloadValues(this.props.tab.id);
}
async reloadVersions(release: HelmRelease) {
async reloadVersions(release: HelmRelease | null | undefined) {
if (!release) {
return;
}
this.version = null;
this.version = undefined;
this.versions.clear();
const versions = await helmChartStore.getVersions(this.release.getChart());
const versions = await helmChartStore.getVersions(release.getChart());
this.versions.replace(versions);
this.version = this.versions[0];
@@ -109,7 +109,10 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
});
upgrade = async () => {
if (this.error) return null;
if (this.error || !this.release || !this.version || !this.value) {
return null;
}
const { version, repo } = this.version;
const releaseName = this.release.getName();
const releaseNs = this.release.getNs();
@@ -122,18 +125,14 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
return (
<p>
Release <b>{releaseName}</b> successfully upgraded to version <b>{version}</b>
{"Release "}
<b>{releaseName}</b>
{" successfully upgraded to version "}
<b>{version}</b>
</p>
);
};
formatVersionLabel = ({ value }: SelectOption<IChartVersion>) => {
const chartName = this.release.getChart();
const { repo, version } = value;
return `${repo}/${chartName}-${version}`;
};
render() {
const { tabId, release, value, error, onChange, onError, upgrade, versions, version } = this;
const { className } = this.props;
@@ -142,21 +141,30 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
return <Spinner center />;
}
const currentVersion = release.getVersion();
const versionOptions = versions.map(version => ({
value: version,
label: `${version.repo}/${release.getChart()}-${version.version}`,
}));
const controlsAndInfo = (
<div className="upgrade flex gaps align-center">
<span>Release</span> <Badge label={release.getName()}/>
<span>Namespace</span> <Badge label={release.getNs()}/>
<span>Version</span> <Badge label={currentVersion}/>
<span>Release</span>
{" "}
<Badge label={release.getName()} />
<span>Namespace</span>
{" "}
<Badge label={release.getNs()} />
<span>Version</span>
{" "}
<Badge label={currentVersion} />
<span>Upgrade version</span>
<Select
<Select<ChartVersion, SelectOption<ChartVersion>, false>
id="char-version-input"
className="chart-version"
menuPlacement="top"
themeName="outlined"
value={version}
options={versions}
formatOptionLabel={this.formatVersionLabel}
onChange={({ value }: SelectOption) => this.version = value}
options={versionOptions}
onChange={option => this.version = option?.value}
/>
</div>
);
@@ -173,7 +181,7 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
/>
<EditorPanel
tabId={tabId}
value={value}
value={value ?? ""}
onChange={onChange}
onError={onError}
/>