mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Turn on strict mode in tsconfig.json, some helpful lints, and required cleanup where strictness necessitates it (#5195)
* Turn on strict mode in tsconfig.json - Add route, clusterRoute, and payloadValidatedClusterRoute helper functions to improve types with backend routes - Turn on the following new lints: - react/jsx-first-prop-new-line - react/jsx-wrap-multilines - react/jsx-one-expression-per-line - react/jsx-max-props-per-line - react/jsx-indent - react/jsx-indent-props Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix build Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace KubeObject scope strings with enum Signed-off-by: Sebastian Malton <sebastian@malton.name> * Revert package.json version changes Signed-off-by: Sebastian Malton <sebastian@malton.name> * revert move hostedCluster(Id) Signed-off-by: Sebastian Malton <sebastian@malton.name> * change some type param names to be not single letters Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove copy-extension-themes Signed-off-by: Sebastian Malton <sebastian@malton.name> * add new make clean action Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix build to not use webpack for generating types Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube-object-menu.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix select.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * revert move fileNameMigration to index Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix ref logic error Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix log-resource-selector.test.tsx tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-store.test.ts test by overriding createStorage to not touch file system Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cluster.test.ts tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube=api.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fixed hotbar-store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kubeconfig-manager.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cluster-role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix pods.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix delete-cluster-dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix daemonset.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix replicaset.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix statefulsets/dialog/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix replicasets/scale-dialog/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix deployments.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix deployments/scale/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cronjob.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix stateful-set.api.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix deployment.api.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix api-manager.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix statefulset.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix job.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix pods.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix scroll-spy.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix hotbar-remove-command.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog-entity-registry.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix welcome.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix verify-that-all-routes-have-route-component.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix pod-tolerations.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * better fix for previous 3 fixes, plus also select.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube-object-menu.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix app-paths.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-tabs.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix isReactNode typing Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix sub-title.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix drawer.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix list-layout.tsx and header.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix error-boundary.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix upgrade-chart/store.ts and dock-tab.store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix install-chart/store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix edit-resource/store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix create-resource/store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix namespace-select.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix namespace-select-filter.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix crd-list.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix wrong types for extensions Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix circular dependency Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix circular dependency on catalogCategoryRegistry Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix api-kube Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix type errors, most <Select /> errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * fixing more type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * some more fixing type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * convert all KubeApis to injectable with legacy global backups Signed-off-by: Sebastian Malton <sebastian@malton.name> * factor out into a common file all the exports Signed-off-by: Sebastian Malton <sebastian@malton.name> * convert all KubeObjectStores to injectable with legacy global backups Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix lint Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove unused legacy KubeApi globals Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix bad previous commit Signed-off-by: Sebastian Malton <sebastian@malton.name> * more crash fixing Signed-off-by: Sebastian Malton <sebastian@malton.name> * try and fix behavioural tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix sidebar-and-tab-navigation-for-core.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix sidebar-and-tab-navigation-for-extensions.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-using-application-menu.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make ThemeStore non-singleton and fix navigation-to-terminal-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * extensions.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog-entity-registry.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-using-application-menu.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix log-resource-selector.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-tabs.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix delete-cluster-dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-kubernetes-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-editor-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-proxy-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-using-application-menu.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-application-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix select.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix hotbar-remove-command.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cluster-role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-extension-specific-preferences.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-telemetry-preferences.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix closing-preferences.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-editor-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-proxy-preferences.test.ts - Fix other type errors too Signed-off-by: Sebastian Malton <sebastian@malton.name> * final tweaks Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add more tsconfig files, fix bug in <Catalog> - Make all of history, navigation injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all of kube-details-params/ and navigate/ to injectable - This fixes a runtime error that was encountered during testing Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix runtime errors on renderer - remove all static uses of `createPageParam` (and then removed the legacy global) - Made LensRendererExtension and LensMainExtension just used dependencies and not the getLegacyDi - Fixed circular dep in extension-loader Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move registerStore calls to after injectMany Signed-off-by: Sebastian Malton <sebastian@malton.name> * replace all the rest of the legacy uses of apiManager Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix stack overflow and cycles in DI Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix NamespaceSelectFilter not opening Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix WizardStep and AddNamespaceDialog Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix KubeApi's not being registered Signed-off-by: Sebastian Malton <sebastian@malton.name> * cleanup WindowManager Signed-off-by: Sebastian Malton <sebastian@malton.name> * Proper fix for Wizard, fix NamespaceStore.subscribe Signed-off-by: Sebastian Malton <sebastian@malton.name> * Rewrite withTooltip to be more type correct - Fixes mobx related "too many recursive actions" error - Change all the uses of withTooltips to be functional components Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add e2e test to cover kube api registration Signed-off-by: Sebastian Malton <sebastian@malton.name> * cleanup internal-commands Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove cast in <Animate> Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix command-palette e2e test Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix type error after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix test name Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix lint Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix code to help CodeQL scanner Signed-off-by: Sebastian Malton <sebastian@malton.name> * update intree extension lock files Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix build-extensions picking wrong @types/react Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix tests from rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix type error Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make KubeconfigSyncManager more injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix crash in test mode for Dialog Signed-off-by: Sebastian Malton <sebastian@malton.name> * make Select snapshots deterministic Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix new type error Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube-object.store.test.ts typing Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix merge build issues Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix snapshots after merge Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix lint after merge Signed-off-by: Sebastian Malton <sebastian@malton.name> * reexport BaseKubeJsonApiObjectMetadata Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix typo in terminalSpawningPool Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove duplicate license header Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix typo to waitUntilDefined Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove iter use from getLegacyGlobalDiForExtensionApi Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove complex createStorage override Signed-off-by: Sebastian Malton <sebastian@malton.name> * override logger with mocks only when needed for tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove specialized overrideStore flags for getDiForUnitTesting Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove unnecessary | undefined types from the exactOptionalFieldTypes experiment Signed-off-by: Sebastian Malton <sebastian@malton.name> * use more descriptive name for local test mocks Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove unnecessary addition to 'make clean' target Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove oddity of KubeObjectStore.getById(undefined) being allowed Signed-off-by: Sebastian Malton <sebastian@malton.name> * rename KubeObject.getDescriptor in favour of name without fundemental JS meaning Signed-off-by: Sebastian Malton <sebastian@malton.name> * Simplify legacyRegisterApi when working in behaviour unit tests - Don't emit within main environment as there should be no auto registering there Signed-off-by: Sebastian Malton <sebastian@malton.name> * change confusing variable name in ReactiveDuration Signed-off-by: Sebastian Malton <sebastian@malton.name> * make visitor pattern more explicit for Entity contextMenuOpen Signed-off-by: Sebastian Malton <sebastian@malton.name> * toggleDetails -> toggleKubeDetailsPane is more specific Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove outdated comment Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix bug where LensExtension dependencies are not set Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests from the revert of react 18 Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix more tests from merge Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix typings with new is-compatible-extension tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * more type fixing Signed-off-by: Sebastian Malton <sebastian@malton.name> * Revert in-tree extension versions Signed-off-by: Sebastian Malton <sebastian@malton.name> * Improve name of guarding injectable for stores and apis - New name better implies that it is just a guard state and does not do anything Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add helper for <Select>.isMulti for storing in a Set<Value> Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix is-compatible-extension.test.ts types Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
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");
|
||||
|
||||
+3
-3
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user