({
- [WorkspaceStore.defaultId]: new Workspace({
- id: WorkspaceStore.defaultId,
- name: "default"
- })
- });
-
@computed get currentWorkspace(): Workspace {
return this.getById(this.currentWorkspaceId);
}
diff --git a/src/extensions/interfaces/index.ts b/src/extensions/interfaces/index.ts
index c91d8cdd19..7b1c601537 100644
--- a/src/extensions/interfaces/index.ts
+++ b/src/extensions/interfaces/index.ts
@@ -1 +1 @@
-export * from "./registrations";
\ No newline at end of file
+export * from "./registrations";
diff --git a/src/extensions/interfaces/registrations.ts b/src/extensions/interfaces/registrations.ts
index 47c63062ea..ff51d9a824 100644
--- a/src/extensions/interfaces/registrations.ts
+++ b/src/extensions/interfaces/registrations.ts
@@ -5,4 +5,4 @@ export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../re
export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry";
export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry";
export type { PageMenuRegistration, ClusterPageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry";
-export type { StatusBarRegistration } from "../registries/status-bar-registry";
\ No newline at end of file
+export type { StatusBarRegistration } from "../registries/status-bar-registry";
diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts
index 25afaa76fe..8b9b132114 100644
--- a/src/extensions/lens-renderer-extension.ts
+++ b/src/extensions/lens-renderer-extension.ts
@@ -2,6 +2,7 @@ import type { AppPreferenceRegistration, ClusterFeatureRegistration, ClusterPage
import type { Cluster } from "../main/cluster";
import { LensExtension } from "./lens-extension";
import { getExtensionPageUrl } from "./registries/page-registry";
+import { CommandRegistration } from "./registries/command-registry";
export class LensRendererExtension extends LensExtension {
globalPages: PageRegistration[] = [];
@@ -14,6 +15,7 @@ export class LensRendererExtension extends LensExtension {
statusBarItems: StatusBarRegistration[] = [];
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
+ commands: CommandRegistration[] = [];
async navigate(pageId?: string, params?: P) {
const { navigate } = await import("../renderer/navigation");
diff --git a/src/extensions/registries/command-registry.ts b/src/extensions/registries/command-registry.ts
new file mode 100644
index 0000000000..0b1fc0252c
--- /dev/null
+++ b/src/extensions/registries/command-registry.ts
@@ -0,0 +1,37 @@
+// Extensions API -> Commands
+
+import type { Cluster } from "../../main/cluster";
+import type { Workspace } from "../../common/workspace-store";
+import { BaseRegistry } from "./base-registry";
+import { action } from "mobx";
+import { LensExtension } from "../lens-extension";
+
+export type CommandContext = {
+ cluster?: Cluster;
+ workspace?: Workspace;
+};
+
+export interface CommandRegistration {
+ id: string;
+ title: string;
+ scope: "cluster" | "global";
+ action: (context: CommandContext) => void;
+ isActive?: (context: CommandContext) => boolean;
+}
+
+export class CommandRegistry extends BaseRegistry {
+ @action
+ add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) {
+ const itemArray = [items].flat();
+
+ const newIds = itemArray.map((item) => item.id);
+ const currentIds = this.getItems().map((item) => item.id);
+
+ const filteredIds = newIds.filter((id) => !currentIds.includes(id));
+ const filteredItems = itemArray.filter((item) => filteredIds.includes(item.id));
+
+ return super.add(filteredItems, extension);
+ }
+}
+
+export const commandRegistry = new CommandRegistry();
diff --git a/src/extensions/registries/status-bar-registry.ts b/src/extensions/registries/status-bar-registry.ts
index 88c4132d30..e0454fe77e 100644
--- a/src/extensions/registries/status-bar-registry.ts
+++ b/src/extensions/registries/status-bar-registry.ts
@@ -3,7 +3,18 @@
import React from "react";
import { BaseRegistry } from "./base-registry";
-export interface StatusBarRegistration {
+interface StatusBarComponents {
+ Item?: React.ComponentType;
+}
+
+interface StatusBarRegistrationV2 {
+ components?: StatusBarComponents; // has to be optional for backwards compatability
+}
+
+export interface StatusBarRegistration extends StatusBarRegistrationV2 {
+ /**
+ * @deprecated use components.Item instead
+ */
item?: React.ReactNode;
}
diff --git a/src/extensions/renderer-api/components.ts b/src/extensions/renderer-api/components.ts
index 68dd5d6510..55de99bf80 100644
--- a/src/extensions/renderer-api/components.ts
+++ b/src/extensions/renderer-api/components.ts
@@ -13,6 +13,9 @@ export * from "../../renderer/components/select";
export * from "../../renderer/components/slider";
export * from "../../renderer/components/input/input";
+// command-overlay
+export { CommandOverlay } from "../../renderer/components/command-palette";
+
// other components
export * from "../../renderer/components/icon";
export * from "../../renderer/components/tooltip";
@@ -38,4 +41,4 @@ export * from "../../renderer/components/+events/kube-event-details";
// specific exports
export * from "../../renderer/components/status-brick";
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
-export { createPodLogsTab } from "../../renderer/components/dock/pod-logs.store";
+export { logTabStore } from "../../renderer/components/dock/log-tab.store";
diff --git a/src/extensions/renderer-api/k8s-api.ts b/src/extensions/renderer-api/k8s-api.ts
index fe04550fb7..071d8365ab 100644
--- a/src/extensions/renderer-api/k8s-api.ts
+++ b/src/extensions/renderer-api/k8s-api.ts
@@ -14,6 +14,7 @@ export { ConfigMap, configMapApi } from "../../renderer/api/endpoints";
export { Secret, secretsApi, ISecretRef } from "../../renderer/api/endpoints";
export { ReplicaSet, replicaSetApi } from "../../renderer/api/endpoints";
export { ResourceQuota, resourceQuotaApi } from "../../renderer/api/endpoints";
+export { LimitRange, limitRangeApi } from "../../renderer/api/endpoints";
export { HorizontalPodAutoscaler, hpaApi } from "../../renderer/api/endpoints";
export { PodDisruptionBudget, pdbApi } from "../../renderer/api/endpoints";
export { Service, serviceApi } from "../../renderer/api/endpoints";
@@ -46,6 +47,7 @@ export type { ConfigMapsStore } from "../../renderer/components/+config-maps/con
export type { SecretsStore } from "../../renderer/components/+config-secrets/secrets.store";
export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/replicasets.store";
export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/resource-quotas.store";
+export type { LimitRangesStore } from "../../renderer/components/+config-limit-ranges/limit-ranges.store";
export type { HPAStore } from "../../renderer/components/+config-autoscalers/hpa.store";
export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store";
export type { ServiceStore } from "../../renderer/components/+network-services/services.store";
diff --git a/src/extensions/renderer-api/kube-object-status.ts b/src/extensions/renderer-api/kube-object-status.ts
index f609d736fe..616ead1bb2 100644
--- a/src/extensions/renderer-api/kube-object-status.ts
+++ b/src/extensions/renderer-api/kube-object-status.ts
@@ -8,4 +8,4 @@ export enum KubeObjectStatusLevel {
INFO = 1,
WARNING = 2,
CRITICAL = 3
-}
\ No newline at end of file
+}
diff --git a/src/extensions/renderer-api/theming.ts b/src/extensions/renderer-api/theming.ts
index f819036803..b3da69bdbc 100644
--- a/src/extensions/renderer-api/theming.ts
+++ b/src/extensions/renderer-api/theming.ts
@@ -2,4 +2,4 @@ import { themeStore } from "../../renderer/theme.store";
export function getActiveTheme() {
return themeStore.activeTheme;
-}
\ No newline at end of file
+}
diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts
index b3f0442cc2..4b11a19879 100644
--- a/src/main/__test__/cluster.test.ts
+++ b/src/main/__test__/cluster.test.ts
@@ -126,6 +126,7 @@ describe("create clusters", () => {
};
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
+ jest.spyOn(Cluster.prototype, "canUseWatchApi").mockReturnValue(Promise.resolve(true));
jest.spyOn(Cluster.prototype, "canI")
.mockImplementation((attr: V1ResourceAttributes): Promise => {
expect(attr.namespace).toBe("default");
diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts
index dd9ed97e69..ed2bf4250b 100644
--- a/src/main/app-updater.ts
+++ b/src/main/app-updater.ts
@@ -1,20 +1,96 @@
-import { autoUpdater } from "electron-updater";
+import { autoUpdater, UpdateInfo } from "electron-updater";
import logger from "./logger";
+import { isDevelopment, isTestEnv } from "../common/vars";
+import { delay } from "../common/utils";
+import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
+import { ipcMain } from "electron";
-export class AppUpdater {
- static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24; // once a day
+let installVersion: null | string = null;
- static checkForUpdates() {
- return autoUpdater.checkForUpdatesAndNotify();
- }
-
- constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) {
- autoUpdater.logger = logger;
- }
-
- public start() {
- setInterval(AppUpdater.checkForUpdates, this.updateInterval);
-
- return AppUpdater.checkForUpdates();
+function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) {
+ if (arg.doUpdate) {
+ if (arg.now) {
+ logger.info(`${AutoUpdateLogPrefix}: User chose to update now`);
+ autoUpdater.on("update-downloaded", () => autoUpdater.quitAndInstall());
+ autoUpdater.downloadUpdate().catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download or install update`, { error }));
+ } else {
+ logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`);
+ autoUpdater.autoInstallOnAppQuit = true;
+ autoUpdater.downloadUpdate()
+ .catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download update`, { error }));
+ }
+ } else {
+ logger.info(`${AutoUpdateLogPrefix}: User chose not to update`);
+ }
+}
+
+/**
+ * starts the automatic update checking
+ * @param interval milliseconds between interval to check on, defaults to 24h
+ */
+export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void {
+ if (isDevelopment || isTestEnv) {
+ return;
+ }
+
+ autoUpdater.logger = logger;
+ autoUpdater.autoDownload = false;
+ autoUpdater.autoInstallOnAppQuit = false;
+
+ autoUpdater
+ .on("update-available", (args: UpdateInfo) => {
+ if (autoUpdater.autoInstallOnAppQuit) {
+ // a previous auto-update loop was completed with YES+LATER, check if same version
+ if (installVersion === args.version) {
+ // same version, don't broadcast
+ return;
+ }
+ }
+
+ /**
+ * This should be always set to false here because it is the reasonable
+ * default. Namely, if a don't auto update to a version that the user
+ * didn't ask for.
+ */
+ autoUpdater.autoInstallOnAppQuit = false;
+ installVersion = args.version;
+
+ try {
+ const backchannel = `auto-update:${args.version}`;
+
+ ipcMain.removeAllListeners(backchannel); // only one handler should be present
+
+ // make sure that the handler is in place before broadcasting (prevent race-condition)
+ onceCorrect({
+ source: ipcMain,
+ channel: backchannel,
+ listener: handleAutoUpdateBackChannel,
+ verifier: areArgsUpdateAvailableToBackchannel,
+ });
+ logger.info(`${AutoUpdateLogPrefix}: broadcasting update available`, { backchannel, version: args.version });
+ broadcastMessage(UpdateAvailableChannel, backchannel, args);
+ } catch (error) {
+ logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error });
+ installVersion = undefined;
+ }
+ });
+
+ async function helper() {
+ while (true) {
+ await checkForUpdates();
+ await delay(interval);
+ }
+ }
+
+ helper();
+}
+
+export async function checkForUpdates(): Promise {
+ try {
+ logger.info(`š” Checking for app updates`);
+
+ await autoUpdater.checkForUpdates();
+ } catch (error) {
+ logger.error(`${AutoUpdateLogPrefix}: failed with an error`, { error: String(error) });
}
}
diff --git a/src/main/cluster-detectors/base-cluster-detector.ts b/src/main/cluster-detectors/base-cluster-detector.ts
index 9d52e1a70e..885f96c33e 100644
--- a/src/main/cluster-detectors/base-cluster-detector.ts
+++ b/src/main/cluster-detectors/base-cluster-detector.ts
@@ -31,4 +31,4 @@ export class BaseClusterDetector {
},
});
}
-}
\ No newline at end of file
+}
diff --git a/src/main/cluster-detectors/cluster-id-detector.ts b/src/main/cluster-detectors/cluster-id-detector.ts
index 2e0cc694ff..810955afae 100644
--- a/src/main/cluster-detectors/cluster-id-detector.ts
+++ b/src/main/cluster-detectors/cluster-id-detector.ts
@@ -23,4 +23,4 @@ export class ClusterIdDetector extends BaseClusterDetector {
return response.metadata.uid;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/cluster-detectors/detector-registry.ts b/src/main/cluster-detectors/detector-registry.ts
index 43c56153c9..b1d1b73447 100644
--- a/src/main/cluster-detectors/detector-registry.ts
+++ b/src/main/cluster-detectors/detector-registry.ts
@@ -48,4 +48,4 @@ detectorRegistry.add(ClusterIdDetector);
detectorRegistry.add(LastSeenDetector);
detectorRegistry.add(VersionDetector);
detectorRegistry.add(DistributionDetector);
-detectorRegistry.add(NodesCountDetector);
\ No newline at end of file
+detectorRegistry.add(NodesCountDetector);
diff --git a/src/main/cluster-detectors/last-seen-detector.ts b/src/main/cluster-detectors/last-seen-detector.ts
index e648d5f2f9..0a9bcf9f74 100644
--- a/src/main/cluster-detectors/last-seen-detector.ts
+++ b/src/main/cluster-detectors/last-seen-detector.ts
@@ -11,4 +11,4 @@ export class LastSeenDetector extends BaseClusterDetector {
return { value: new Date().toJSON(), accuracy: 100 };
}
-}
\ No newline at end of file
+}
diff --git a/src/main/cluster-detectors/nodes-count-detector.ts b/src/main/cluster-detectors/nodes-count-detector.ts
index 0ece5dd080..45584df5bd 100644
--- a/src/main/cluster-detectors/nodes-count-detector.ts
+++ b/src/main/cluster-detectors/nodes-count-detector.ts
@@ -16,4 +16,4 @@ export class NodesCountDetector extends BaseClusterDetector {
return response.items.length;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/cluster-detectors/version-detector.ts b/src/main/cluster-detectors/version-detector.ts
index 8080ef57a1..b19979db8a 100644
--- a/src/main/cluster-detectors/version-detector.ts
+++ b/src/main/cluster-detectors/version-detector.ts
@@ -16,4 +16,4 @@ export class VersionDetector extends BaseClusterDetector {
return response.gitVersion;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts
index 5717c7278d..dfcda98203 100644
--- a/src/main/cluster-manager.ts
+++ b/src/main/cluster-manager.ts
@@ -1,7 +1,7 @@
import "../common/cluster-ipc";
import type http from "http";
import { ipcMain } from "electron";
-import { autorun } from "mobx";
+import { autorun, reaction } from "mobx";
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store";
import { Cluster } from "./cluster";
import logger from "./logger";
@@ -12,14 +12,14 @@ export class ClusterManager extends Singleton {
constructor(public readonly port: number) {
super();
// auto-init clusters
- autorun(() => {
- clusterStore.enabledClustersList.forEach(cluster => {
- if (!cluster.initialized) {
+ reaction(() => clusterStore.enabledClustersList, (clusters) => {
+ clusters.forEach((cluster) => {
+ if (!cluster.initialized && !cluster.initializing) {
logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta());
cluster.init(port);
}
});
- });
+ }, { fireImmediately: true });
// auto-stop removed clusters
autorun(() => {
diff --git a/src/main/cluster.ts b/src/main/cluster.ts
index 9fc0fb7cc3..8e7bc507e2 100644
--- a/src/main/cluster.ts
+++ b/src/main/cluster.ts
@@ -48,6 +48,7 @@ export interface ClusterState {
isAdmin: boolean;
allowedNamespaces: string[]
allowedResources: string[]
+ isGlobalWatchEnabled: boolean;
}
/**
@@ -85,6 +86,13 @@ export class Cluster implements ClusterModel, ClusterState {
whenInitialized = when(() => this.initialized);
whenReady = when(() => this.ready);
+ /**
+ * Is cluster object initializinng on-going
+ *
+ * @observable
+ */
+ @observable initializing = false;
+
/**
* Is cluster object initialized
*
@@ -171,6 +179,12 @@ export class Cluster implements ClusterModel, ClusterState {
*/
@observable isAdmin = false;
+ /**
+ * Global watch-api accessibility , e.g. "/api/v1/services?watch=1"
+ *
+ * @observable
+ */
+ @observable isGlobalWatchEnabled = false;
/**
* Preferences
*
@@ -184,7 +198,7 @@ export class Cluster implements ClusterModel, ClusterState {
*/
@observable metadata: ClusterMetadata = {};
/**
- * List of allowed namespaces
+ * List of allowed namespaces verified via K8S::SelfSubjectAccessReview api
*
* @observable
*/
@@ -197,7 +211,7 @@ export class Cluster implements ClusterModel, ClusterState {
*/
@observable allowedResources: string[] = [];
/**
- * List of accessible namespaces
+ * List of accessible namespaces provided by user in the Cluster Settings
*
* @observable
*/
@@ -218,7 +232,7 @@ export class Cluster implements ClusterModel, ClusterState {
* @computed
*/
@computed get name() {
- return this.preferences.clusterName ||Ā this.contextName;
+ return this.preferences.clusterName || this.contextName;
}
/**
@@ -279,8 +293,10 @@ export class Cluster implements ClusterModel, ClusterState {
* @param port port where internal auth proxy is listening
* @internal
*/
- @action async init(port: number) {
+ @action
+ async init(port: number) {
try {
+ this.initializing = true;
this.contextHandler = new ContextHandler(this);
this.kubeconfigManager = await KubeconfigManager.create(this, this.contextHandler, port);
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
@@ -295,6 +311,8 @@ export class Cluster implements ClusterModel, ClusterState {
id: this.id,
error: err,
});
+ } finally {
+ this.initializing = false;
}
}
@@ -331,7 +349,8 @@ export class Cluster implements ClusterModel, ClusterState {
* @param force force activation
* @internal
*/
- @action async activate(force = false) {
+ @action
+ async activate(force = false) {
if (this.activated && !force) {
return this.pushState();
}
@@ -348,9 +367,7 @@ export class Cluster implements ClusterModel, ClusterState {
await this.refreshConnectionStatus();
if (this.accessible) {
- await this.refreshAllowedResources();
- this.isAdmin = await this.isClusterAdmin();
- this.ready = true;
+ await this.refreshAccessibility();
this.ensureKubectl();
}
this.activated = true;
@@ -370,7 +387,8 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
*/
- @action async reconnect() {
+ @action
+ async reconnect() {
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
this.contextHandler?.stopServer();
await this.contextHandler?.ensureServer();
@@ -389,6 +407,7 @@ export class Cluster implements ClusterModel, ClusterState {
this.accessible = false;
this.ready = false;
this.activated = false;
+ this.allowedNamespaces = [];
this.resourceAccessStatuses.clear();
this.pushState();
}
@@ -397,19 +416,18 @@ export class Cluster implements ClusterModel, ClusterState {
* @internal
* @param opts refresh options
*/
- @action async refresh(opts: ClusterRefreshOptions = {}) {
+ @action
+ async refresh(opts: ClusterRefreshOptions = {}) {
logger.info(`[CLUSTER]: refresh`, this.getMeta());
await this.whenInitialized;
await this.refreshConnectionStatus();
if (this.accessible) {
- this.isAdmin = await this.isClusterAdmin();
- await this.refreshAllowedResources();
+ await this.refreshAccessibility();
if (opts.refreshMetadata) {
this.refreshMetadata();
}
- this.ready = true;
}
this.pushState();
}
@@ -417,7 +435,8 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
*/
- @action async refreshMetadata() {
+ @action
+ async refreshMetadata() {
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
const metadata = await detectorRegistry.detectForCluster(this);
const existingMetadata = this.metadata;
@@ -428,7 +447,20 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
*/
- @action async refreshConnectionStatus() {
+ private async refreshAccessibility(): Promise {
+ this.isAdmin = await this.isClusterAdmin();
+ this.isGlobalWatchEnabled = await this.canUseWatchApi({ resource: "*" });
+
+ await this.refreshAllowedResources();
+
+ this.ready = true;
+ }
+
+ /**
+ * @internal
+ */
+ @action
+ async refreshConnectionStatus() {
const connectionStatus = await this.getConnectionStatus();
this.online = connectionStatus > ClusterStatus.Offline;
@@ -438,7 +470,8 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
*/
- @action async refreshAllowedResources() {
+ @action
+ async refreshAllowedResources() {
this.allowedNamespaces = await this.getAllowedNamespaces();
this.allowedResources = await this.getAllowedResources();
}
@@ -561,6 +594,17 @@ export class Cluster implements ClusterModel, ClusterState {
});
}
+ /**
+ * @internal
+ */
+ async canUseWatchApi(customizeResource: V1ResourceAttributes = {}): Promise {
+ return this.canI({
+ verb: "watch",
+ resource: "*",
+ ...customizeResource,
+ });
+ }
+
toJSON(): ClusterModel {
const model: ClusterModel = {
id: this.id,
@@ -594,6 +638,7 @@ export class Cluster implements ClusterModel, ClusterState {
isAdmin: this.isAdmin,
allowedNamespaces: this.allowedNamespaces,
allowedResources: this.allowedResources,
+ isGlobalWatchEnabled: this.isGlobalWatchEnabled,
};
return toJS(state, {
@@ -665,7 +710,7 @@ export class Cluster implements ClusterModel, ClusterState {
for (const namespace of this.allowedNamespaces.slice(0, 10)) {
if (!this.resourceAccessStatuses.get(apiResource)) {
const result = await this.canI({
- resource: apiResource.resource,
+ resource: apiResource.apiName,
group: apiResource.group,
verb: "list",
namespace
@@ -680,9 +725,19 @@ export class Cluster implements ClusterModel, ClusterState {
return apiResources
.filter((resource) => this.resourceAccessStatuses.get(resource))
- .map(apiResource => apiResource.resource);
+ .map(apiResource => apiResource.apiName);
} catch (error) {
return [];
}
}
+
+ isAllowedResource(kind: string): boolean {
+ const apiResource = apiResources.find(resource => resource.kind === kind || resource.apiName === kind);
+
+ if (apiResource) {
+ return this.allowedResources.includes(apiResource.apiName);
+ }
+
+ return true; // allowed by default for other resources
+ }
}
diff --git a/src/main/developer-tools.ts b/src/main/developer-tools.ts
index 3a3d5009f8..d0d7e2ae01 100644
--- a/src/main/developer-tools.ts
+++ b/src/main/developer-tools.ts
@@ -1,9 +1,12 @@
+import logger from "./logger";
+
/**
* Installs Electron developer tools in the development build.
* The dependency is not bundled to the production build.
*/
export const installDeveloperTools = async () => {
if (process.env.NODE_ENV === "development") {
+ logger.info("š¤ Installing developer tools");
const { default: devToolsInstaller, REACT_DEVELOPER_TOOLS } = await import("electron-devtools-installer");
return devToolsInstaller([REACT_DEVELOPER_TOOLS]);
diff --git a/src/main/helm/__mocks__/helm-chart-manager.ts b/src/main/helm/__mocks__/helm-chart-manager.ts
new file mode 100644
index 0000000000..e832a937cb
--- /dev/null
+++ b/src/main/helm/__mocks__/helm-chart-manager.ts
@@ -0,0 +1,108 @@
+import { HelmRepo, HelmRepoManager } from "../helm-repo-manager";
+
+export class HelmChartManager {
+ private cache: any = {};
+ private repo: HelmRepo;
+
+ constructor(repo: HelmRepo){
+ this.cache = HelmRepoManager.cache;
+ this.repo = repo;
+ }
+
+ public async charts(): Promise {
+ switch (this.repo.name) {
+ case "stable":
+ return Promise.resolve({
+ "apm-server": [
+ {
+ apiVersion: "3.0.0",
+ name: "apm-server",
+ version: "2.1.7",
+ repo: "stable",
+ digest: "test"
+ },
+ {
+ apiVersion: "3.0.0",
+ name: "apm-server",
+ version: "2.1.6",
+ repo: "stable",
+ digest: "test"
+ }
+ ],
+ "redis": [
+ {
+ apiVersion: "3.0.0",
+ name: "apm-server",
+ version: "1.0.0",
+ repo: "stable",
+ digest: "test"
+ },
+ {
+ apiVersion: "3.0.0",
+ name: "apm-server",
+ version: "0.0.9",
+ repo: "stable",
+ digest: "test"
+ }
+ ]
+ });
+ case "experiment":
+ return Promise.resolve({
+ "fairwind": [
+ {
+ apiVersion: "3.0.0",
+ name: "fairwind",
+ version: "0.0.1",
+ repo: "experiment",
+ digest: "test"
+ },
+ {
+ apiVersion: "3.0.0",
+ name: "fairwind",
+ version: "0.0.2",
+ repo: "experiment",
+ digest: "test",
+ deprecated: true
+ }
+ ]
+ });
+ case "bitnami":
+ return Promise.resolve({
+ "hotdog": [
+ {
+ apiVersion: "3.0.0",
+ name: "hotdog",
+ version: "1.0.1",
+ repo: "bitnami",
+ digest: "test"
+ },
+ {
+ apiVersion: "3.0.0",
+ name: "hotdog",
+ version: "1.0.2",
+ repo: "bitnami",
+ digest: "test",
+ }
+ ],
+ "pretzel": [
+ {
+ apiVersion: "3.0.0",
+ name: "pretzel",
+ version: "1.0",
+ repo: "bitnami",
+ digest: "test",
+ },
+ {
+ apiVersion: "3.0.0",
+ name: "pretzel",
+ version: "1.0.1",
+ repo: "bitnami",
+ digest: "test"
+ }
+ ]
+ });
+ default:
+ return Promise.resolve({});
+ }
+ }
+}
diff --git a/src/main/helm/__tests__/helm-service.test.ts b/src/main/helm/__tests__/helm-service.test.ts
new file mode 100644
index 0000000000..8c1e82ef0a
--- /dev/null
+++ b/src/main/helm/__tests__/helm-service.test.ts
@@ -0,0 +1,104 @@
+import { helmService } from "../helm-service";
+import { repoManager } from "../helm-repo-manager";
+
+jest.spyOn(repoManager, "init").mockImplementation();
+
+jest.mock("../helm-chart-manager");
+
+describe("Helm Service tests", () => {
+ test("list charts without deprecated ones", async () => {
+ jest.spyOn(repoManager, "repositories").mockImplementation(async () => {
+ return [
+ { name: "stable", url: "stableurl" },
+ { name: "experiment", url: "experimenturl" }
+ ];
+ });
+
+ const charts = await helmService.listCharts();
+
+ expect(charts).toEqual({
+ stable: {
+ "apm-server": [
+ {
+ apiVersion: "3.0.0",
+ name: "apm-server",
+ version: "2.1.7",
+ repo: "stable",
+ digest: "test"
+ },
+ {
+ apiVersion: "3.0.0",
+ name: "apm-server",
+ version: "2.1.6",
+ repo: "stable",
+ digest: "test"
+ }
+ ],
+ "redis": [
+ {
+ apiVersion: "3.0.0",
+ name: "apm-server",
+ version: "1.0.0",
+ repo: "stable",
+ digest: "test"
+ },
+ {
+ apiVersion: "3.0.0",
+ name: "apm-server",
+ version: "0.0.9",
+ repo: "stable",
+ digest: "test"
+ }
+ ]
+ },
+ experiment: {}
+ });
+ });
+
+ test("list charts sorted by version in descending order", async () => {
+ jest.spyOn(repoManager, "repositories").mockImplementation(async () => {
+ return [
+ { name: "bitnami", url: "bitnamiurl" }
+ ];
+ });
+
+ const charts = await helmService.listCharts();
+
+ expect(charts).toEqual({
+ bitnami: {
+ "hotdog": [
+ {
+ apiVersion: "3.0.0",
+ name: "hotdog",
+ version: "1.0.2",
+ repo: "bitnami",
+ digest: "test",
+ },
+ {
+ apiVersion: "3.0.0",
+ name: "hotdog",
+ version: "1.0.1",
+ repo: "bitnami",
+ digest: "test"
+ },
+ ],
+ "pretzel": [
+ {
+ apiVersion: "3.0.0",
+ name: "pretzel",
+ version: "1.0.1",
+ repo: "bitnami",
+ digest: "test",
+ },
+ {
+ apiVersion: "3.0.0",
+ name: "pretzel",
+ version: "1.0",
+ repo: "bitnami",
+ digest: "test"
+ }
+ ]
+ }
+ });
+ });
+});
diff --git a/src/main/helm/helm-chart-manager.ts b/src/main/helm/helm-chart-manager.ts
index cf4a8e5ace..69619a56d4 100644
--- a/src/main/helm/helm-chart-manager.ts
+++ b/src/main/helm/helm-chart-manager.ts
@@ -4,9 +4,10 @@ import { HelmRepo, HelmRepoManager } from "./helm-repo-manager";
import logger from "../logger";
import { promiseExec } from "../promise-exec";
import { helmCli } from "./helm-cli";
+import type { RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
type CachedYaml = {
- entries: any; // todo: types
+ entries: RepoHelmChartList
};
export class HelmChartManager {
@@ -24,15 +25,15 @@ export class HelmChartManager {
return charts[name];
}
- public async charts(): Promise {
+ public async charts(): Promise {
try {
const cachedYaml = await this.cachedYaml();
return cachedYaml["entries"];
} catch(error) {
- logger.error(error);
+ logger.error("HELM-CHART-MANAGER]: failed to list charts", { error });
- return [];
+ return {};
}
}
diff --git a/src/main/helm/helm-service.ts b/src/main/helm/helm-service.ts
index 1918268075..f7445cebd4 100644
--- a/src/main/helm/helm-service.ts
+++ b/src/main/helm/helm-service.ts
@@ -1,8 +1,10 @@
+import semver from "semver";
import { Cluster } from "../cluster";
import logger from "../logger";
import { repoManager } from "./helm-repo-manager";
import { HelmChartManager } from "./helm-chart-manager";
import { releaseManager } from "./helm-release-manager";
+import { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
class HelmService {
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
@@ -10,7 +12,7 @@ class HelmService {
}
public async listCharts() {
- const charts: any = {};
+ const charts: HelmChartList = {};
await repoManager.init();
const repositories = await repoManager.repositories();
@@ -18,14 +20,10 @@ class HelmService {
for (const repo of repositories) {
charts[repo.name] = {};
const manager = new HelmChartManager(repo);
- let entries = await manager.charts();
+ const sortedCharts = this.sortChartsByVersion(await manager.charts());
+ const enabledCharts = this.excludeDeprecatedChartGroups(sortedCharts);
- entries = this.excludeDeprecated(entries);
-
- for (const key in entries) {
- entries[key] = entries[key][0];
- }
- charts[repo.name] = entries;
+ charts[repo.name] = enabledCharts;
}
return charts;
@@ -96,20 +94,30 @@ class HelmService {
return { message: output };
}
- protected excludeDeprecated(entries: any) {
- for (const key in entries) {
- entries[key] = entries[key].filter((entry: any) => {
- if (Array.isArray(entry)) {
- return entry[0]["deprecated"] != true;
- }
+ private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) {
+ const groups = new Map(Object.entries(chartGroups));
- return entry["deprecated"] != true;
+ for (const [chartName, charts] of groups) {
+ if (charts[0].deprecated) {
+ groups.delete(chartName);
+ }
+ }
+
+ return Object.fromEntries(groups);
+ }
+
+ private sortChartsByVersion(chartGroups: RepoHelmChartList) {
+ for (const key in chartGroups) {
+ chartGroups[key] = chartGroups[key].sort((first, second) => {
+ const firstVersion = semver.coerce(first.version || 0);
+ const secondVersion = semver.coerce(second.version || 0);
+
+ return semver.compare(secondVersion, firstVersion);
});
}
- return entries;
+ return chartGroups;
}
-
}
export const helmService = new HelmService();
diff --git a/src/main/index.ts b/src/main/index.ts
index 265c91f6d4..c98595fa35 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -4,13 +4,12 @@ import "../common/system-ca";
import "../common/prometheus-providers";
import * as Mobx from "mobx";
import * as LensExtensions from "../extensions/core-api";
-import { app, dialog, powerMonitor } from "electron";
+import { app, autoUpdater, dialog, powerMonitor } from "electron";
import { appName } from "../common/vars";
import path from "path";
import { LensProxy } from "./lens-proxy";
import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager";
-import { AppUpdater } from "./app-updater";
import { shellSync } from "./shell-sync";
import { getFreePort } from "./port";
import { mangleProxyEnv } from "./proxy-env";
@@ -26,6 +25,9 @@ import { InstalledExtension, extensionDiscovery } from "../extensions/extension-
import type { LensExtensionId } from "../extensions/lens-extension";
import { installDeveloperTools } from "./developer-tools";
import { filesystemProvisionerStore } from "./extension-filesystem";
+import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
+import { bindBroadcastHandlers } from "../common/ipc";
+import { startUpdateChecking } from "./app-updater";
const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number;
@@ -61,20 +63,20 @@ if (process.env.LENS_DISABLE_GPU) {
app.on("ready", async () => {
logger.info(`š Starting Lens from "${workingDir}"`);
+ logger.info("š Syncing shell environment");
await shellSync();
+ bindBroadcastHandlers();
+
powerMonitor.on("shutdown", () => {
app.exit();
});
- const updater = new AppUpdater();
-
- updater.start();
-
registerFileProtocol("static", __static);
await installDeveloperTools();
+ logger.info("š¾ Loading stores");
// preload
await Promise.all([
userStore.load(),
@@ -86,6 +88,7 @@ app.on("ready", async () => {
// find free port
try {
+ logger.info("š Getting free port for LensProxy server");
proxyPort = await getFreePort();
} catch (error) {
logger.error(error);
@@ -98,6 +101,7 @@ app.on("ready", async () => {
// run proxy
try {
+ logger.info("š Starting LensProxy");
// eslint-disable-next-line unused-imports/no-unused-vars-ts
proxyServer = LensProxy.create(proxyPort, clusterManager);
} catch (error) {
@@ -106,9 +110,27 @@ app.on("ready", async () => {
app.exit();
}
+ // test proxy connection
+ try {
+ logger.info("š Testing LensProxy connection ...");
+ const versionFromProxy = await getAppVersionFromProxyServer(proxyPort);
+
+ if (getAppVersion() !== versionFromProxy) {
+ logger.error(`Proxy server responded with invalid response`);
+ }
+ logger.info("ā” LensProxy connection OK");
+ } catch (error) {
+ logger.error("Checking proxy server connection failed", error);
+ }
+
extensionLoader.init();
extensionDiscovery.init();
+
+ logger.info("š„ļø Starting WindowManager");
windowManager = WindowManager.getInstance(proxyPort);
+ windowManager.whenLoaded.then(() => startUpdateChecking());
+
+ logger.info("š§© Initializing extensions");
// call after windowManager to see splash earlier
try {
@@ -145,14 +167,25 @@ app.on("activate", (event, hasVisibleWindows) => {
}
});
+/**
+ * This variable should is used so that `autoUpdater.installAndQuit()` works
+ */
+let blockQuit = true;
+
+autoUpdater.on("before-quit-for-update", () => blockQuit = false);
+
// Quit app on Cmd+Q (MacOS)
app.on("will-quit", (event) => {
logger.info("APP:QUIT");
appEventBus.emit({name: "app", action: "close"});
- event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
+
clusterManager?.stop(); // close cluster connections
- return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
+ if (blockQuit) {
+ event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
+
+ return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
+ }
});
// Extensions-api runtime exports
diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts
index ebfd2a6a98..7e0d6ed5c7 100644
--- a/src/main/kubectl.ts
+++ b/src/main/kubectl.ts
@@ -23,10 +23,10 @@ const kubectlMap: Map = new Map([
["1.14", "1.14.10"],
["1.15", "1.15.11"],
["1.16", "1.16.15"],
- ["1.17", bundledVersion],
- ["1.18", "1.18.14"],
- ["1.19", "1.19.5"],
- ["1.20", "1.20.0"]
+ ["1.17", "1.17.17"],
+ ["1.18", bundledVersion],
+ ["1.19", "1.19.7"],
+ ["1.20", "1.20.2"]
]);
const packageMirrors: Map = new Map([
["default", "https://storage.googleapis.com/kubernetes-release/release"],
diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts
index e4f6ab4a34..177e4d11d2 100644
--- a/src/main/lens-proxy.ts
+++ b/src/main/lens-proxy.ts
@@ -29,7 +29,7 @@ export class LensProxy {
listen(port = this.port): this {
this.proxyServer = this.buildCustomProxy().listen(port);
- logger.info(`LensProxy server has started at ${this.origin}`);
+ logger.info(`[LENS-PROXY]: Proxy server has started at ${this.origin}`);
return this;
}
@@ -194,7 +194,8 @@ export class LensProxy {
if (proxyTarget) {
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
- res.setHeader("Access-Control-Allow-Origin", this.origin);
+ // this should be safe because we have already validated cluster uuid
+ res.setHeader("Access-Control-Allow-Origin", "*");
return proxy.web(req, res, proxyTarget);
}
diff --git a/src/main/menu.ts b/src/main/menu.ts
index 2cddbb1b01..57c6ccab5e 100644
--- a/src/main/menu.ts
+++ b/src/main/menu.ts
@@ -10,6 +10,7 @@ import { extensionsURL } from "../renderer/components/+extensions/extensions.rou
import { menuRegistry } from "../extensions/registries/menu-registry";
import logger from "./logger";
import { exitApp } from "./exit-app";
+import { broadcastMessage } from "../common/ipc";
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
@@ -173,6 +174,14 @@ export function buildMenu(windowManager: WindowManager) {
const viewMenu: MenuItemConstructorOptions = {
label: "View",
submenu: [
+ {
+ label: "Command Palette...",
+ accelerator: "Shift+CmdOrCtrl+P",
+ click() {
+ broadcastMessage("command-palette:open");
+ }
+ },
+ { type: "separator" },
{
label: "Back",
accelerator: "CmdOrCtrl+[",
diff --git a/src/main/router.ts b/src/main/router.ts
index 896893a592..bb49aacdab 100644
--- a/src/main/router.ts
+++ b/src/main/router.ts
@@ -5,7 +5,7 @@ import path from "path";
import { readFile } from "fs-extra";
import { Cluster } from "./cluster";
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
-import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
+import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, versionRoute } from "./routes";
import logger from "./logger";
export interface RouterRequestOpts {
@@ -143,11 +143,9 @@ export class Router {
this.handleStaticFile(params.path, response, req);
});
+ this.router.add({ method: "get", path: "/version"}, versionRoute.getVersion.bind(versionRoute));
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute));
- // Watch API
- this.router.add({ method: "get", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute));
-
// Metrics API
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute));
diff --git a/src/main/routes/index.ts b/src/main/routes/index.ts
index 5bc5b3f3dd..c194d8f8b2 100644
--- a/src/main/routes/index.ts
+++ b/src/main/routes/index.ts
@@ -1,6 +1,6 @@
export * from "./kubeconfig-route";
export * from "./metrics-route";
export * from "./port-forward-route";
-export * from "./watch-route";
export * from "./helm-route";
export * from "./resource-applier-route";
+export * from "./version-route";
diff --git a/src/main/routes/version-route.ts b/src/main/routes/version-route.ts
new file mode 100644
index 0000000000..81ada9eca7
--- /dev/null
+++ b/src/main/routes/version-route.ts
@@ -0,0 +1,13 @@
+import { LensApiRequest } from "../router";
+import { LensApi } from "../lens-api";
+import { getAppVersion } from "../../common/utils";
+
+class VersionRoute extends LensApi {
+ public async getVersion(request: LensApiRequest) {
+ const { response } = request;
+
+ this.respondJson(response, { version: getAppVersion()}, 200);
+ }
+}
+
+export const versionRoute = new VersionRoute();
diff --git a/src/main/routes/watch-route.ts b/src/main/routes/watch-route.ts
deleted file mode 100644
index eb9f007eae..0000000000
--- a/src/main/routes/watch-route.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import { LensApiRequest } from "../router";
-import { LensApi } from "../lens-api";
-import { Watch, KubeConfig } from "@kubernetes/client-node";
-import { ServerResponse } from "http";
-import { Request } from "request";
-import logger from "../logger";
-
-class ApiWatcher {
- private apiUrl: string;
- private response: ServerResponse;
- private watchRequest: Request;
- private watch: Watch;
- private processor: NodeJS.Timeout;
- private eventBuffer: any[] = [];
-
- constructor(apiUrl: string, kubeConfig: KubeConfig, response: ServerResponse) {
- this.apiUrl = apiUrl;
- this.watch = new Watch(kubeConfig);
- this.response = response;
- }
-
- public async start() {
- if (this.processor) {
- clearInterval(this.processor);
- }
- this.processor = setInterval(() => {
- const events = this.eventBuffer.splice(0);
-
- events.map(event => this.sendEvent(event));
- this.response.flushHeaders();
- }, 50);
- this.watchRequest = await this.watch.watch(this.apiUrl, {}, this.watchHandler.bind(this), this.doneHandler.bind(this));
- }
-
- public stop() {
- if (!this.watchRequest) { return; }
-
- if (this.processor) {
- clearInterval(this.processor);
- }
- logger.debug(`Stopping watcher for api: ${this.apiUrl}`);
-
- try {
- this.watchRequest.abort();
- this.sendEvent({
- type: "STREAM_END",
- url: this.apiUrl,
- status: 410,
- });
- logger.debug("watch aborted");
- } catch (error) {
- logger.error(`Watch abort errored:${error}`);
- }
- }
-
- private watchHandler(phase: string, obj: any) {
- this.eventBuffer.push({
- type: phase,
- object: obj
- });
- }
-
- private doneHandler(error: Error) {
- if (error) logger.warn(`watch ended: ${error.toString()}`);
- this.watchRequest.abort();
- }
-
- private sendEvent(evt: any) {
- // convert to "text/event-stream" format
- this.response.write(`data: ${JSON.stringify(evt)}\n\n`);
- }
-}
-
-class WatchRoute extends LensApi {
-
- public async routeWatch(request: LensApiRequest) {
- const { response, cluster} = request;
- const apis: string[] = request.query.getAll("api");
- const watchers: ApiWatcher[] = [];
-
- if (!apis.length) {
- this.respondJson(response, {
- message: "Empty request. Query params 'api' are not provided.",
- example: "?api=/api/v1/pods&api=/api/v1/nodes",
- }, 400);
-
- return;
- }
-
- response.setHeader("Content-Type", "text/event-stream");
- response.setHeader("Cache-Control", "no-cache");
- response.setHeader("Connection", "keep-alive");
- logger.debug(`watch using kubeconfig:${JSON.stringify(cluster.getProxyKubeconfig(), null, 2)}`);
-
- apis.forEach(apiUrl => {
- const watcher = new ApiWatcher(apiUrl, cluster.getProxyKubeconfig(), response);
-
- watcher.start();
- watchers.push(watcher);
- });
-
- request.raw.req.on("close", () => {
- logger.debug("Watch request closed");
- watchers.map(watcher => watcher.stop());
- });
-
- request.raw.req.on("end", () => {
- logger.debug("Watch request ended");
- watchers.map(watcher => watcher.stop());
- });
-
- }
-}
-
-export const watchRoute = new WatchRoute();
diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts
index be04649a31..9e5af371f7 100644
--- a/src/main/shell-session.ts
+++ b/src/main/shell-session.ts
@@ -108,7 +108,7 @@ export class ShellSession extends EventEmitter {
if(isWindows) {
env["SystemRoot"] = process.env.SystemRoot;
- env["PTYSHELL"] = "powershell.exe";
+ env["PTYSHELL"] = process.env.SHELL || "powershell.exe";
env["PATH"] = pathStr;
} else if(typeof(process.env.SHELL) != "undefined") {
env["PTYSHELL"] = process.env.SHELL;
diff --git a/src/main/tray.ts b/src/main/tray.ts
index 47f641ad72..3d6d2dd624 100644
--- a/src/main/tray.ts
+++ b/src/main/tray.ts
@@ -1,9 +1,9 @@
import path from "path";
import packageInfo from "../../package.json";
-import { dialog, Menu, NativeImage, Tray } from "electron";
+import { Menu, NativeImage, Tray } from "electron";
import { autorun } from "mobx";
import { showAbout } from "./menu";
-import { AppUpdater } from "./app-updater";
+import { checkForUpdates } from "./app-updater";
import { WindowManager } from "./window-manager";
import { clusterStore } from "../common/cluster-store";
import { workspaceStore } from "../common/workspace-store";
@@ -62,16 +62,6 @@ function buildTray(icon: string | NativeImage, menu: Menu, windowManager: Window
function createTrayMenu(windowManager: WindowManager): Menu {
return Menu.buildFromTemplate([
- {
- label: "About Lens",
- async click() {
- // note: argument[1] (browserWindow) not available when app is not focused / hidden
- const browserWindow = await windowManager.ensureMainWindow();
-
- showAbout(browserWindow);
- },
- },
- { type: "separator" },
{
label: "Open Lens",
async click() {
@@ -112,16 +102,17 @@ function createTrayMenu(windowManager: WindowManager): Menu {
{
label: "Check for updates",
async click() {
- const result = await AppUpdater.checkForUpdates();
+ await checkForUpdates();
+ await windowManager.ensureMainWindow();
+ },
+ },
+ {
+ label: "About Lens",
+ async click() {
+ // note: argument[1] (browserWindow) not available when app is not focused / hidden
+ const browserWindow = await windowManager.ensureMainWindow();
- if (!result) {
- const browserWindow = await windowManager.ensureMainWindow();
-
- dialog.showMessageBoxSync(browserWindow, {
- message: "No updates available",
- type: "info",
- });
- }
+ showAbout(browserWindow);
},
},
{ type: "separator" },
diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts
index bf7458afa0..c092e186cb 100644
--- a/src/main/window-manager.ts
+++ b/src/main/window-manager.ts
@@ -1,5 +1,5 @@
import type { ClusterId } from "../common/cluster-store";
-import { observable } from "mobx";
+import { observable, when } from "mobx";
import { app, BrowserWindow, dialog, shell, webContents } from "electron";
import windowStateKeeper from "electron-window-state";
import { appEventBus } from "../common/event-bus";
@@ -8,6 +8,7 @@ import { initMenu } from "./menu";
import { initTray } from "./tray";
import { Singleton } from "../common/utils";
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
+import logger from "./logger";
export class WindowManager extends Singleton {
protected mainWindow: BrowserWindow;
@@ -15,6 +16,9 @@ export class WindowManager extends Singleton {
protected windowState: windowStateKeeper.State;
protected disposers: Record = {};
+ @observable mainViewInitiallyLoaded = false;
+ whenLoaded = when(() => this.mainViewInitiallyLoaded);
+
@observable activeClusterId: ClusterId;
constructor(protected proxyPort: number) {
@@ -81,16 +85,26 @@ export class WindowManager extends Singleton {
this.splashWindow = null;
app.dock?.hide(); // hide icon in dock (mac-os)
});
+
+ this.mainWindow.webContents.on("did-fail-load", (_event, code, desc) => {
+ logger.error(`[WINDOW-MANAGER]:Ā Failed to load Main window`, { code, desc });
+ });
+
+ this.mainWindow.webContents.on("did-finish-load", () => {
+ logger.info("[WINDOW-MANAGER]: Main window loaded");
+ });
}
try {
if (showSplash) await this.showSplash();
+ logger.info(`[WINDOW-MANAGER]:Ā Loading Main window from url: ${this.mainUrl} ...`);
await this.mainWindow.loadURL(this.mainUrl);
this.mainWindow.show();
this.splashWindow?.close();
setTimeout(() => {
appEventBus.emit({ name: "app", action: "start" });
}, 1000);
+ this.mainViewInitiallyLoaded = true;
} catch (err) {
dialog.showErrorBox("ERROR!", err.toString());
}
diff --git a/src/migrations/cluster-store/index.ts b/src/migrations/cluster-store/index.ts
index c546fdaeda..4a71d4f7ad 100644
--- a/src/migrations/cluster-store/index.ts
+++ b/src/migrations/cluster-store/index.ts
@@ -18,4 +18,4 @@ export default {
...version270Beta1,
...version360Beta1,
...snap
-};
\ No newline at end of file
+};
diff --git a/src/renderer/api/__tests__/api-manager.test.ts b/src/renderer/api/__tests__/api-manager.test.ts
new file mode 100644
index 0000000000..29ce7bd189
--- /dev/null
+++ b/src/renderer/api/__tests__/api-manager.test.ts
@@ -0,0 +1,40 @@
+import { ingressStore } from "../../components/+network-ingresses/ingress.store";
+import { apiManager } from "../api-manager";
+import { KubeApi } from "../kube-api";
+
+class TestApi extends KubeApi {
+
+ protected async checkPreferredVersion() {
+ return;
+ }
+}
+
+describe("ApiManager", () => {
+ describe("registerApi", () => {
+ it("re-register store if apiBase changed", async () => {
+ const apiBase = "apis/v1/foo";
+ const fallbackApiBase = "/apis/extensions/v1beta1/foo";
+ const kubeApi = new TestApi({
+ apiBase,
+ fallbackApiBases: [fallbackApiBase],
+ checkPreferredVersion: true,
+ });
+
+ apiManager.registerApi(apiBase, kubeApi);
+
+ // Define to use test api for ingress store
+ Object.defineProperty(ingressStore, "api", { value: kubeApi });
+ apiManager.registerStore(ingressStore, [kubeApi]);
+
+ // Test that store is returned with original apiBase
+ expect(apiManager.getStore(kubeApi)).toBe(ingressStore);
+
+ // Change apiBase similar as checkPreferredVersion does
+ Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase });
+ apiManager.registerApi(fallbackApiBase, kubeApi);
+
+ // Test that store is returned with new apiBase
+ expect(apiManager.getStore(kubeApi)).toBe(ingressStore);
+ });
+ });
+});
diff --git a/src/renderer/api/__tests__/kube-api.test.ts b/src/renderer/api/__tests__/kube-api.test.ts
index 41078e77a3..7481bd096a 100644
--- a/src/renderer/api/__tests__/kube-api.test.ts
+++ b/src/renderer/api/__tests__/kube-api.test.ts
@@ -79,4 +79,4 @@ describe("KubeApi", () => {
expect(kubeApi.apiPrefix).toEqual("/apis");
expect(kubeApi.apiGroup).toEqual("extensions");
});
-});
\ No newline at end of file
+});
diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts
index 629a0f29c2..90e63f692b 100644
--- a/src/renderer/api/api-manager.ts
+++ b/src/renderer/api/api-manager.ts
@@ -2,16 +2,16 @@ import type { KubeObjectStore } from "../kube-object.store";
import { action, observable } from "mobx";
import { autobind } from "../utils";
-import { KubeApi } from "./kube-api";
+import { KubeApi, parseKubeApi } from "./kube-api";
@autobind()
export class ApiManager {
private apis = observable.map();
- private stores = observable.map();
+ private stores = observable.map();
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) {
if (typeof pathOrCallback === "string") {
- return this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase);
+ return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase);
}
return Array.from(this.apis.values()).find(pathOrCallback ?? (() => true));
@@ -23,6 +23,12 @@ export class ApiManager {
registerApi(apiBase: string, api: KubeApi) {
if (!this.apis.has(apiBase)) {
+ this.stores.forEach((store) => {
+ if(store.api === api) {
+ this.stores.set(apiBase, store);
+ }
+ });
+
this.apis.set(apiBase, api);
}
}
@@ -46,12 +52,12 @@ export class ApiManager {
@action
registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) {
apis.forEach(api => {
- this.stores.set(api, store);
+ this.stores.set(api.apiBase, store);
});
}
getStore(api: string | KubeApi): S {
- return this.stores.get(this.resolveApi(api)) as S;
+ return this.stores.get(this.resolveApi(api)?.apiBase) as S;
}
}
diff --git a/src/renderer/api/endpoints/events.api.ts b/src/renderer/api/endpoints/events.api.ts
index df9aa540a4..4fb21aaaa5 100644
--- a/src/renderer/api/endpoints/events.api.ts
+++ b/src/renderer/api/endpoints/events.api.ts
@@ -28,7 +28,7 @@ export class KubeEvent extends KubeObject {
firstTimestamp: string;
lastTimestamp: string;
count: number;
- type: string;
+ type: "Normal" | "Warning" | string;
eventTime: null;
reportingComponent: string;
reportingInstance: string;
diff --git a/src/renderer/api/endpoints/helm-charts.api.ts b/src/renderer/api/endpoints/helm-charts.api.ts
index a1fd497798..02b5b0dbee 100644
--- a/src/renderer/api/endpoints/helm-charts.api.ts
+++ b/src/renderer/api/endpoints/helm-charts.api.ts
@@ -3,11 +3,8 @@ import { apiBase } from "../index";
import { stringify } from "querystring";
import { autobind } from "../../utils";
-interface IHelmChartList {
- [repo: string]: {
- [name: string]: HelmChart;
- };
-}
+export type RepoHelmChartList = Record;
+export type HelmChartList = Record;
export interface IHelmChartDetails {
readme: string;
@@ -22,12 +19,12 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: {
export const helmChartsApi = {
list() {
return apiBase
- .get(endpoint())
+ .get(endpoint())
.then(data => {
return Object
.values(data)
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), [])
- .map(HelmChart.create);
+ .map(([chart]) => HelmChart.create(chart));
});
},
@@ -86,7 +83,7 @@ export class HelmChart {
tillerVersion?: string;
getId() {
- return this.digest;
+ return `${this.apiVersion}/${this.name}@${this.getAppVersion()}`;
}
getName() {
diff --git a/src/renderer/api/endpoints/helm-releases.api.ts b/src/renderer/api/endpoints/helm-releases.api.ts
index 84e095721b..fb3eae1a79 100644
--- a/src/renderer/api/endpoints/helm-releases.api.ts
+++ b/src/renderer/api/endpoints/helm-releases.api.ts
@@ -57,6 +57,7 @@ export interface IReleaseRevision {
updated: string;
status: string;
chart: string;
+ app_version: string;
description: string;
}
@@ -197,10 +198,9 @@ export class HelmRelease implements ItemObject {
}
getUpdated(humanize = true, compact = true) {
- const now = new Date().getTime();
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
const updatedDate = new Date(updated).getTime();
- const diff = now - updatedDate;
+ const diff = Date.now() - updatedDate;
if (humanize) {
return formatDuration(diff, compact);
diff --git a/src/renderer/api/endpoints/index.ts b/src/renderer/api/endpoints/index.ts
index f1202b9122..5ab54e7c3a 100644
--- a/src/renderer/api/endpoints/index.ts
+++ b/src/renderer/api/endpoints/index.ts
@@ -14,6 +14,7 @@ export * from "./events.api";
export * from "./hpa.api";
export * from "./ingress.api";
export * from "./job.api";
+export * from "./limit-range.api";
export * from "./namespaces.api";
export * from "./network-policy.api";
export * from "./nodes.api";
diff --git a/src/renderer/api/endpoints/limit-range.api.ts b/src/renderer/api/endpoints/limit-range.api.ts
new file mode 100644
index 0000000000..bbb3941c87
--- /dev/null
+++ b/src/renderer/api/endpoints/limit-range.api.ts
@@ -0,0 +1,57 @@
+import { KubeObject } from "../kube-object";
+import { KubeApi } from "../kube-api";
+import { autobind } from "../../utils";
+
+export enum LimitType {
+ CONTAINER = "Container",
+ POD = "Pod",
+ PVC = "PersistentVolumeClaim",
+}
+
+export enum Resource {
+ MEMORY = "memory",
+ CPU = "cpu",
+ STORAGE = "storage",
+ EPHEMERAL_STORAGE = "ephemeral-storage",
+}
+
+export enum LimitPart {
+ MAX = "max",
+ MIN = "min",
+ DEFAULT = "default",
+ DEFAULT_REQUEST = "defaultRequest",
+ MAX_LIMIT_REQUEST_RATIO = "maxLimitRequestRatio",
+}
+
+type LimitRangeParts = Partial>>;
+
+export interface LimitRangeItem extends LimitRangeParts {
+ type: string
+}
+
+@autobind()
+export class LimitRange extends KubeObject {
+ static kind = "LimitRange";
+ static namespaced = true;
+ static apiBase = "/api/v1/limitranges";
+
+ spec: {
+ limits: LimitRangeItem[];
+ };
+
+ getContainerLimits() {
+ return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER);
+ }
+
+ getPodLimits() {
+ return this.spec.limits.filter(limit => limit.type === LimitType.POD);
+ }
+
+ getPVCLimits() {
+ return this.spec.limits.filter(limit => limit.type === LimitType.PVC);
+ }
+}
+
+export const limitRangeApi = new KubeApi({
+ objectConstructor: LimitRange,
+});
diff --git a/src/renderer/api/endpoints/persistent-volume.api.ts b/src/renderer/api/endpoints/persistent-volume.api.ts
index dd5dbb616e..db286db062 100644
--- a/src/renderer/api/endpoints/persistent-volume.api.ts
+++ b/src/renderer/api/endpoints/persistent-volume.api.ts
@@ -63,10 +63,16 @@ export class PersistentVolume extends KubeObject {
return this.status.phase || "-";
}
- getClaimRefName() {
- const { claimRef } = this.spec;
+ getStorageClass(): string {
+ return this.spec.storageClassName;
+ }
- return claimRef ? claimRef.name : "";
+ getClaimRefName(): string {
+ return this.spec.claimRef?.name ?? "";
+ }
+
+ getStorageClassName() {
+ return this.spec.storageClassName || "";
}
}
diff --git a/src/renderer/api/endpoints/pods.api.ts b/src/renderer/api/endpoints/pods.api.ts
index eca15251a7..3176cae4d2 100644
--- a/src/renderer/api/endpoints/pods.api.ts
+++ b/src/renderer/api/endpoints/pods.api.ts
@@ -104,6 +104,9 @@ export interface IPodContainer {
configMapRef?: {
name: string;
};
+ secretRef?: {
+ name: string;
+ }
}[];
volumeMounts?: {
name: string;
diff --git a/src/renderer/api/json-api.ts b/src/renderer/api/json-api.ts
index 49c2cb1a28..df12b08ab7 100644
--- a/src/renderer/api/json-api.ts
+++ b/src/renderer/api/json-api.ts
@@ -3,7 +3,7 @@
import { stringify } from "querystring";
import { EventEmitter } from "../../common/event-emitter";
import { cancelableFetch } from "../utils/cancelableFetch";
-
+import { randomBytes } from "crypto";
export interface JsonApiData {
}
@@ -55,6 +55,34 @@ export class JsonApi {
return this.request(path, params, { ...reqInit, method: "get" });
}
+ getResponse(path: string, params?: P, init: RequestInit = {}): Promise {
+ const reqPath = `${this.config.apiBase}${path}`;
+ const subdomain = randomBytes(2).toString("hex");
+ let reqUrl = `http://${subdomain}.${window.location.host}${reqPath}`; // hack around browser connection limits (chromium allows 6 per domain)
+ const reqInit: RequestInit = { ...init };
+ const { query } = params || {} as P;
+
+ if (!reqInit.method) {
+ reqInit.method = "get";
+ }
+
+ if (query) {
+ const queryString = stringify(query);
+
+ reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
+ }
+
+ const infoLog: JsonApiLog = {
+ method: reqInit.method.toUpperCase(),
+ reqUrl: reqPath,
+ reqInit,
+ };
+
+ this.writeLog({ ...infoLog });
+
+ return fetch(reqUrl, reqInit);
+ }
+
post(path: string, params?: P, reqInit: RequestInit = {}) {
return this.request(path, params, { ...reqInit, method: "post" });
}
diff --git a/src/renderer/api/kube-api.ts b/src/renderer/api/kube-api.ts
index 8a3a2517c2..448cd9da8f 100644
--- a/src/renderer/api/kube-api.ts
+++ b/src/renderer/api/kube-api.ts
@@ -8,8 +8,10 @@ import { apiManager } from "./api-manager";
import { apiKube } from "./index";
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
import { KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api";
-import { IKubeObjectConstructor, KubeObject } from "./kube-object";
-import { kubeWatchApi } from "./kube-watch-api";
+import { IKubeObjectConstructor, KubeObject, KubeStatus } from "./kube-object";
+import byline from "byline";
+import { IKubeWatchEvent } from "./kube-watch-api";
+import { ReadableWebToNodeStream } from "../utils/readableStream";
export interface IKubeApiOptions {
/**
@@ -91,15 +93,15 @@ export function ensureObjectSelfLink(api: KubeApi, object: KubeJsonApiData) {
}
}
+export type KubeApiWatchCallback = (data: IKubeWatchEvent, error: any) => void;
+
+export type KubeApiWatchOptions = {
+ namespace: string;
+ callback?: KubeApiWatchCallback;
+ abortController?: AbortController
+};
+
export class KubeApi {
- static parseApi = parseKubeApi;
-
- static watchAll(...apis: KubeApi[]) {
- const disposers = apis.map(api => api.watch());
-
- return () => disposers.forEach(unwatch => unwatch());
- }
-
readonly kind: string;
readonly apiBase: string;
readonly apiPrefix: string;
@@ -112,6 +114,7 @@ export class KubeApi {
public objectConstructor: IKubeObjectConstructor;
protected request: KubeJsonApi;
protected resourceVersions = new Map();
+ protected watchDisposer: () => void;
constructor(protected options: IKubeApiOptions) {
const {
@@ -124,7 +127,7 @@ export class KubeApi {
if (!options.apiBase) {
options.apiBase = objectConstructor.apiBase;
}
- const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = KubeApi.parseApi(options.apiBase);
+ const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase);
this.kind = kind;
this.isNamespaced = isNamespaced;
@@ -157,7 +160,7 @@ export class KubeApi {
for (const apiUrl of apiBases) {
// Split e.g. "/apis/extensions/v1beta1/ingresses" to parts
- const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = KubeApi.parseApi(apiUrl);
+ const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = parseKubeApi(apiUrl);
// Request available resources
try {
@@ -269,6 +272,7 @@ export class KubeApi {
}
protected parseResponse(data: KubeJsonApiData | KubeJsonApiData[] | KubeJsonApiDataList, namespace?: string): any {
+ if (!data) return;
const KubeObjectConstructor = this.objectConstructor;
if (KubeObject.isJsonApiData(data)) {
@@ -365,8 +369,98 @@ export class KubeApi {
});
}
- watch(): () => void {
- return kubeWatchApi.subscribe(this);
+ watch(opts: KubeApiWatchOptions = { namespace: "" }): () => void {
+ if (!opts.abortController) {
+ opts.abortController = new AbortController();
+ }
+ let errorReceived = false;
+ let timedRetry: NodeJS.Timeout;
+ const { abortController, namespace, callback } = opts;
+
+ abortController.signal.addEventListener("abort", () => {
+ clearTimeout(timedRetry);
+ });
+
+ const watchUrl = this.getWatchUrl(namespace);
+ const responsePromise = this.request.getResponse(watchUrl, null, {
+ signal: abortController.signal
+ });
+
+ responsePromise.then((response) => {
+ if (!response.ok && !abortController.signal.aborted) {
+ callback?.(null, response);
+
+ return;
+ }
+ const nodeStream = new ReadableWebToNodeStream(response.body);
+
+ ["end", "close", "error"].forEach((eventName) => {
+ nodeStream.on(eventName, () => {
+ if (errorReceived) return; // kubernetes errors should be handled in a callback
+
+ clearTimeout(timedRetry);
+ timedRetry = setTimeout(() => { // we did not get any kubernetes errors so let's retry
+ if (abortController.signal.aborted) return;
+
+ this.watch({...opts, namespace, callback});
+ }, 1000);
+ });
+ });
+
+ const stream = byline(nodeStream);
+
+ stream.on("data", (line) => {
+ try {
+ const event: IKubeWatchEvent = JSON.parse(line);
+
+ if (event.type === "ERROR" && event.object.kind === "Status") {
+ errorReceived = true;
+ callback(null, new KubeStatus(event.object as any));
+
+ return;
+ }
+
+ this.modifyWatchEvent(event);
+
+ if (callback) {
+ callback(event, null);
+ }
+ } catch (ignore) {
+ // ignore parse errors
+ }
+ });
+ }, (error) => {
+ if (error instanceof DOMException) return; // AbortController rejects, we can ignore it
+
+ callback?.(null, error);
+ }).catch((error) => {
+ callback?.(null, error);
+ });
+
+ const disposer = () => {
+ abortController.abort();
+ };
+
+ return disposer;
+ }
+
+ protected modifyWatchEvent(event: IKubeWatchEvent) {
+
+ switch (event.type) {
+ case "ADDED":
+ case "DELETED":
+
+ case "MODIFIED": {
+ ensureObjectSelfLink(this, event.object);
+
+ const { namespace, resourceVersion } = event.object.metadata;
+
+ this.setResourceVersion(namespace, resourceVersion);
+ this.setResourceVersion("", resourceVersion);
+
+ break;
+ }
+ }
}
}
diff --git a/src/renderer/api/kube-json-api.ts b/src/renderer/api/kube-json-api.ts
index 3026a9a956..362ee5438e 100644
--- a/src/renderer/api/kube-json-api.ts
+++ b/src/renderer/api/kube-json-api.ts
@@ -21,7 +21,7 @@ export interface KubeJsonApiData extends JsonApiData {
resourceVersion: string;
continue?: string;
finalizers?: string[];
- selfLink: string;
+ selfLink?: string;
labels?: {
[label: string]: string;
};
diff --git a/src/renderer/api/kube-object.ts b/src/renderer/api/kube-object.ts
index 08bd6401b9..7d0c34de33 100644
--- a/src/renderer/api/kube-object.ts
+++ b/src/renderer/api/kube-object.ts
@@ -40,6 +40,29 @@ export interface IKubeObjectMetadata {
}[];
}
+export interface IKubeStatus {
+ kind: string;
+ apiVersion: string;
+ code: number;
+ message?: string;
+ reason?: string;
+}
+
+export class KubeStatus {
+ public readonly kind = "Status";
+ public readonly apiVersion: string;
+ public readonly code: number;
+ public readonly message: string;
+ public readonly reason: string;
+
+ constructor(data: IKubeStatus) {
+ this.apiVersion = data.apiVersion;
+ this.code = data.code;
+ this.message = data.message || "";
+ this.reason = data.reason || "";
+ }
+}
+
export type IKubeMetaField = keyof IKubeObjectMetadata;
@autobind()
@@ -99,12 +122,15 @@ export class KubeObject implements ItemObject {
return this.metadata.namespace || undefined;
}
- // todo: refactor with named arguments
- getAge(humanize = true, compact = true, fromNow = false) {
+ getTimeDiffFromNow(): number {
+ return Date.now() - new Date(this.metadata.creationTimestamp).getTime();
+ }
+
+ getAge(humanize = true, compact = true, fromNow = false): string | number {
if (fromNow) {
- return moment(this.metadata.creationTimestamp).fromNow();
+ return moment(this.metadata.creationTimestamp).fromNow(); // "string", getTimeDiffFromNow() cannot be used
}
- const diff = new Date().getTime() - new Date(this.metadata.creationTimestamp).getTime();
+ const diff = this.getTimeDiffFromNow();
if (humanize) {
return formatDuration(diff, compact);
diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts
index 78ca25256e..f2f50193db 100644
--- a/src/renderer/api/kube-watch-api.ts
+++ b/src/renderer/api/kube-watch-api.ts
@@ -1,183 +1,131 @@
-// Kubernetes watch-api consumer
+// Kubernetes watch-api client
+// API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
-import { computed, observable, reaction } from "mobx";
-import { stringify } from "querystring";
-import { autobind, EventEmitter } from "../utils";
-import { KubeJsonApiData } from "./kube-json-api";
import type { KubeObjectStore } from "../kube-object.store";
-import { ensureObjectSelfLink, KubeApi } from "./kube-api";
-import { apiManager } from "./api-manager";
-import { apiPrefix, isDevelopment } from "../../common/vars";
-import { getHostedCluster } from "../../common/cluster-store";
+import type { ClusterContext } from "../components/context";
-export interface IKubeWatchEvent {
- type: "ADDED" | "MODIFIED" | "DELETED";
+import plimit from "p-limit";
+import { comparer, IReactionDisposer, observable, reaction, when } from "mobx";
+import { autobind, noop } from "../utils";
+import { KubeApi } from "./kube-api";
+import { KubeJsonApiData } from "./kube-json-api";
+import { isDebugging, isProduction } from "../../common/vars";
+
+export interface IKubeWatchEvent {
+ type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR";
object?: T;
}
-export interface IKubeWatchRouteEvent {
- type: "STREAM_END";
- url: string;
- status: number;
+export interface IKubeWatchSubscribeStoreOptions {
+ namespaces?: string[]; // default: all accessible namespaces
+ preload?: boolean; // preload store items, default: true
+ waitUntilLoaded?: boolean; // subscribe only after loading all stores, default: true
+ loadOnce?: boolean; // check store.isLoaded to skip loading if done already, default: false
}
-export interface IKubeWatchRouteQuery {
- api: string | string[];
+export interface IKubeWatchLog {
+ message: string | string[] | Error;
+ meta?: object;
+ cssStyle?: string;
}
@autobind()
export class KubeWatchApi {
- protected evtSource: EventSource;
- protected onData = new EventEmitter<[IKubeWatchEvent]>();
- protected subscribers = observable.map();
- protected reconnectTimeoutMs = 5000;
- protected maxReconnectsOnError = 10;
- protected reconnectAttempts = this.maxReconnectsOnError;
+ @observable context: ClusterContext = null;
- constructor() {
- reaction(() => this.activeApis, () => this.connect(), {
- fireImmediately: true,
- delay: 500,
- });
+ contextReady = when(() => Boolean(this.context));
+
+ isAllowedApi(api: KubeApi): boolean {
+ return Boolean(this.context?.cluster.isAllowedResource(api.kind));
}
- @computed get activeApis() {
- return Array.from(this.subscribers.keys());
- }
+ preloadStores(stores: KubeObjectStore[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) {
+ const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
+ const preloading: Promise[] = [];
- getSubscribersCount(api: KubeApi) {
- return this.subscribers.get(api) || 0;
- }
+ for (const store of stores) {
+ preloading.push(limitRequests(async () => {
+ if (store.isLoaded && opts.loadOnce) return; // skip
- subscribe(...apis: KubeApi[]) {
- apis.forEach(api => {
- this.subscribers.set(api, this.getSubscribersCount(api) + 1);
- });
-
- return () => apis.forEach(api => {
- const count = this.getSubscribersCount(api) - 1;
-
- if (count <= 0) this.subscribers.delete(api);
- else this.subscribers.set(api, count);
- });
- }
-
- protected getQuery(): Partial {
- const { isAdmin, allowedNamespaces } = getHostedCluster();
+ return store.loadAll({ namespaces: opts.namespaces });
+ }));
+ }
return {
- api: this.activeApis.map(api => {
- if (isAdmin) return api.getWatchUrl();
-
- return allowedNamespaces.map(namespace => api.getWatchUrl(namespace));
- }).flat()
+ loading: Promise.allSettled(preloading),
+ cancelLoading: () => limitRequests.clearQueue(),
};
}
- // todo: maybe switch to websocket to avoid often reconnects
- @autobind()
- protected connect() {
- if (this.evtSource) this.disconnect(); // close previous connection
+ subscribeStores(stores: KubeObjectStore[], opts: IKubeWatchSubscribeStoreOptions = {}): () => void {
+ const { preload = true, waitUntilLoaded = true, loadOnce = false, } = opts;
+ const subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? [];
+ const unsubscribeList: Function[] = [];
+ let isUnsubscribed = false;
- if (!this.activeApis.length) {
+ const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce });
+ let preloading = preload && load();
+ let cancelReloading: IReactionDisposer = noop;
+
+ const subscribe = () => {
+ if (isUnsubscribed) return;
+
+ stores.forEach((store) => {
+ unsubscribeList.push(store.subscribe());
+ });
+ };
+
+ if (preloading) {
+ if (waitUntilLoaded) {
+ preloading.loading.then(subscribe, error => {
+ this.log({
+ message: new Error("Loading stores has failed"),
+ meta: { stores, error, options: opts },
+ });
+ });
+ } else {
+ subscribe();
+ }
+
+ // reload stores only for context namespaces change
+ cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => {
+ preloading?.cancelLoading();
+ unsubscribeList.forEach(unsubscribe => unsubscribe());
+ unsubscribeList.length = 0;
+ preloading = load(namespaces);
+ preloading.loading.then(subscribe);
+ }, {
+ equals: comparer.shallow,
+ });
+ }
+
+ // unsubscribe
+ return () => {
+ if (isUnsubscribed) return;
+ isUnsubscribed = true;
+ cancelReloading();
+ preloading?.cancelLoading();
+ unsubscribeList.forEach(unsubscribe => unsubscribe());
+ unsubscribeList.length = 0;
+ };
+ }
+
+ protected log({ message, cssStyle = "", meta = {} }: IKubeWatchLog) {
+ if (isProduction && !isDebugging) {
return;
}
- const query = this.getQuery();
- const apiUrl = `${apiPrefix}/watch?${stringify(query)}`;
- this.evtSource = new EventSource(apiUrl);
- this.evtSource.onmessage = this.onMessage;
- this.evtSource.onerror = this.onError;
- this.writeLog("CONNECTING", query.api);
- }
-
- reconnect() {
- if (!this.evtSource || this.evtSource.readyState !== EventSource.OPEN) {
- this.reconnectAttempts = this.maxReconnectsOnError;
- this.connect();
- }
- }
-
- protected disconnect() {
- if (!this.evtSource) return;
- this.evtSource.close();
- this.evtSource.onmessage = null;
- this.evtSource = null;
- }
-
- protected onMessage(evt: MessageEvent) {
- if (!evt.data) return;
- const data = JSON.parse(evt.data);
-
- if ((data as IKubeWatchEvent).object) {
- this.onData.emit(data);
- } else {
- this.onRouteEvent(data);
- }
- }
-
- protected async onRouteEvent(event: IKubeWatchRouteEvent) {
- if (event.type === "STREAM_END") {
- this.disconnect();
- const { apiBase, namespace } = KubeApi.parseApi(event.url);
- const api = apiManager.getApi(apiBase);
-
- if (api) {
- try {
- await api.refreshResourceVersion({ namespace });
- this.reconnect();
- } catch (error) {
- console.error("failed to refresh resource version", error);
-
- if (this.subscribers.size > 0) {
- setTimeout(() => {
- this.onRouteEvent(event);
- }, 1000);
- }
- }
- }
- }
- }
-
- protected onError(evt: MessageEvent) {
- const { reconnectAttempts: attemptsRemain, reconnectTimeoutMs } = this;
-
- if (evt.eventPhase === EventSource.CLOSED) {
- if (attemptsRemain > 0) {
- this.reconnectAttempts--;
- setTimeout(() => this.connect(), reconnectTimeoutMs);
- }
- }
- }
-
- protected writeLog(...data: any[]) {
- if (isDevelopment) {
- console.log("%cKUBE-WATCH-API:", `font-weight: bold`, ...data);
- }
- }
-
- addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) {
- const listener = (evt: IKubeWatchEvent) => {
- const { namespace, resourceVersion } = evt.object.metadata;
- const api = apiManager.getApiByKind(evt.object.kind, evt.object.apiVersion);
-
- api.setResourceVersion(namespace, resourceVersion);
- api.setResourceVersion("", resourceVersion);
-
- ensureObjectSelfLink(api, evt.object);
-
- if (store == apiManager.getStore(api)) {
- callback(evt);
- }
+ const logInfo = [`%c[KUBE-WATCH-API]:`, `font-weight: bold; ${cssStyle}`, message].flat().map(String);
+ const logMeta = {
+ time: new Date().toLocaleString(),
+ ...meta,
};
- this.onData.addListener(listener);
-
- return () => this.onData.removeListener(listener);
- }
-
- reset() {
- this.subscribers.clear();
+ if (message instanceof Error) {
+ console.error(...logInfo, logMeta);
+ } else {
+ console.info(...logInfo, logMeta);
+ }
}
}
diff --git a/src/renderer/api/workload-kube-object.ts b/src/renderer/api/workload-kube-object.ts
index e0c6d3f121..185d3d502c 100644
--- a/src/renderer/api/workload-kube-object.ts
+++ b/src/renderer/api/workload-kube-object.ts
@@ -1,7 +1,7 @@
import get from "lodash/get";
import { KubeObject } from "./kube-object";
-interface IToleration {
+export interface IToleration {
key?: string;
operator?: string;
effect?: string;
@@ -82,4 +82,4 @@ export class WorkloadKubeObject extends KubeObject {
return Object.keys(affinity).length;
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx
index a33e9d2503..d31efc5438 100644
--- a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx
+++ b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx
@@ -82,9 +82,9 @@ export class HelmChartDetails extends Component {
{selectedChart.getDescription()}
-
+
-
+
{
onChange={onVersionChange}
/>
-
+
{selectedChart.getHome()}
-
+
{selectedChart.getMaintainers().map(({ name, email, url }) =>
{name}
)}
{selectedChart.getKeywords().length > 0 && (
-
+
{selectedChart.getKeywords().map(key => )}
)}
diff --git a/src/renderer/components/+apps-helm-charts/helm-charts.route.ts b/src/renderer/components/+apps-helm-charts/helm-charts.route.ts
index 9b8aecc499..97a0923d97 100644
--- a/src/renderer/components/+apps-helm-charts/helm-charts.route.ts
+++ b/src/renderer/components/+apps-helm-charts/helm-charts.route.ts
@@ -11,4 +11,4 @@ export interface IHelmChartsRouteParams {
repo?: string;
}
-export const helmChartsURL = buildURL(helmChartsRoute.path);
\ No newline at end of file
+export const helmChartsURL = buildURL(helmChartsRoute.path);
diff --git a/src/renderer/components/+apps-helm-charts/helm-charts.tsx b/src/renderer/components/+apps-helm-charts/helm-charts.tsx
index 8bf5486a55..8e4afe8d83 100644
--- a/src/renderer/components/+apps-helm-charts/helm-charts.tsx
+++ b/src/renderer/components/+apps-helm-charts/helm-charts.tsx
@@ -11,8 +11,11 @@ import { navigation } from "../../navigation";
import { ItemListLayout } from "../item-object-list/item-list-layout";
import { SearchInputUrl } from "../input";
-enum sortBy {
+enum columnId {
name = "name",
+ description = "description",
+ version = "version",
+ appVersion = "app-version",
repo = "repo",
}
@@ -53,13 +56,15 @@ export class HelmCharts extends Component {
return (
<>
chart.getName(),
- [sortBy.repo]: (chart: HelmChart) => chart.getRepository(),
+ [columnId.name]: (chart: HelmChart) => chart.getName(),
+ [columnId.repo]: (chart: HelmChart) => chart.getRepository(),
}}
searchFilters={[
(chart: HelmChart) => chart.getName(),
@@ -67,20 +72,16 @@ export class HelmCharts extends Component {
(chart: HelmChart) => chart.getAppVersion(),
(chart: HelmChart) => chart.getKeywords(),
]}
- filterItems={[
- (items: HelmChart[]) => items.filter(item => !item.deprecated)
- ]}
customizeHeader={() => (
-
+
)}
renderTableHeader={[
- { className: "icon" },
- { title: "Name", className: "name", sortBy: sortBy.name },
- { title: "Description", className: "description" },
- { title: "Version", className: "version" },
- { title: "App Version", className: "app-version" },
- { title: "Repository", className: "repository", sortBy: sortBy.repo },
-
+ { className: "icon", showWithColumn: columnId.name },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { title: "Description", className: "description", id: columnId.description },
+ { title: "Version", className: "version", id: columnId.version },
+ { title: "App Version", className: "app-version", id: columnId.appVersion },
+ { title: "Repository", className: "repository", sortBy: columnId.repo, id: columnId.repo },
]}
renderTableContents={(chart: HelmChart) => [
@@ -93,7 +94,8 @@ export class HelmCharts extends Component {
chart.getDescription(),
chart.getVersion(),
chart.getAppVersion(),
- { title: chart.getRepository(), className: chart.getRepository().toLowerCase() }
+ { title: chart.getRepository(), className: chart.getRepository().toLowerCase() },
+ { className: "menu" }
]}
detailsItem={this.selectedChart}
onDetails={this.showDetails}
diff --git a/src/renderer/components/+apps-helm-charts/index.ts b/src/renderer/components/+apps-helm-charts/index.ts
index a9403c097c..c0649f3f38 100644
--- a/src/renderer/components/+apps-helm-charts/index.ts
+++ b/src/renderer/components/+apps-helm-charts/index.ts
@@ -1,2 +1,2 @@
export * from "./helm-charts";
-export * from "./helm-charts.route";
\ No newline at end of file
+export * from "./helm-charts.route";
diff --git a/src/renderer/components/+apps-releases/index.ts b/src/renderer/components/+apps-releases/index.ts
index 32a4871769..bd80c60404 100644
--- a/src/renderer/components/+apps-releases/index.ts
+++ b/src/renderer/components/+apps-releases/index.ts
@@ -1,2 +1,2 @@
export * from "./releases";
-export * from "./release.route";
\ No newline at end of file
+export * from "./release.route";
diff --git a/src/renderer/components/+apps-releases/release-details.tsx b/src/renderer/components/+apps-releases/release-details.tsx
index 35a1878202..e139569937 100644
--- a/src/renderer/components/+apps-releases/release-details.tsx
+++ b/src/renderer/components/+apps-releases/release-details.tsx
@@ -111,7 +111,7 @@ export class ReleaseDetails extends Component {
return (
-
+
{
/>
@@ -200,7 +200,7 @@ export class ReleaseDetails extends Component {
{release.getChart()}
@@ -226,9 +226,9 @@ export class ReleaseDetails extends Component {
/>
{this.renderValues()}
-
+
{this.renderNotes()}
-
+
{this.renderResources()}
);
diff --git a/src/renderer/components/+apps-releases/release-menu.tsx b/src/renderer/components/+apps-releases/release-menu.tsx
index 3b260fc5ca..e482bb000a 100644
--- a/src/renderer/components/+apps-releases/release-menu.tsx
+++ b/src/renderer/components/+apps-releases/release-menu.tsx
@@ -42,7 +42,7 @@ export class HelmReleaseMenu extends React.Component {
<>
{hasRollback && (
-
+
Rollback
)}
diff --git a/src/renderer/components/+apps-releases/release-rollback-dialog.tsx b/src/renderer/components/+apps-releases/release-rollback-dialog.tsx
index bca0b23c4f..4aef4ec356 100644
--- a/src/renderer/components/+apps-releases/release-rollback-dialog.tsx
+++ b/src/renderer/components/+apps-releases/release-rollback-dialog.tsx
@@ -77,7 +77,8 @@ export class ReleaseRollbackDialog extends React.Component {
themeName="light"
value={revision}
options={revisions}
- formatOptionLabel={({ value }: SelectOption) => `${value.revision} - ${value.chart}`}
+ formatOptionLabel={({ value }: SelectOption) => `${value.revision} - ${value.chart}
+ - ${value.app_version}, updated: ${new Date(value.updated).toLocaleString()}`}
onChange={({ value }: SelectOption) => this.revision = value}
/>
diff --git a/src/renderer/components/+apps-releases/release.store.ts b/src/renderer/components/+apps-releases/release.store.ts
index b6d5c2fb5f..559a90eb01 100644
--- a/src/renderer/components/+apps-releases/release.store.ts
+++ b/src/renderer/components/+apps-releases/release.store.ts
@@ -5,7 +5,7 @@ import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayl
import { ItemStore } from "../../item.store";
import { Secret } from "../../api/endpoints";
import { secretsStore } from "../+config-secrets/secrets.store";
-import { getHostedCluster } from "../../../common/cluster-store";
+import { namespaceStore } from "../+namespaces/namespace.store";
@autobind()
export class ReleaseStore extends ItemStore {
@@ -33,7 +33,7 @@ export class ReleaseStore extends ItemStore {
});
if (amountChanged || labelsChanged) {
- this.loadAll();
+ this.loadFromContextNamespaces();
}
this.releaseSecrets = [...secrets];
});
@@ -58,26 +58,30 @@ export class ReleaseStore extends ItemStore {
}
@action
- async loadAll() {
+ async loadAll(namespaces: string[]) {
this.isLoading = true;
- let items;
try {
- const { isAdmin, allowedNamespaces } = getHostedCluster();
+ const items = await this.loadItems(namespaces);
- items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
- } finally {
- if (items) {
- items = this.sortItems(items);
- this.items.replace(items);
- }
+ this.items.replace(this.sortItems(items));
this.isLoaded = true;
+ } catch (error) {
+ console.error(`Loading Helm Chart releases has failed: ${error}`);
+ } finally {
this.isLoading = false;
}
}
- async loadItems(namespaces?: string[]) {
- if (!namespaces) {
+ async loadFromContextNamespaces(): Promise {
+ return this.loadAll(namespaceStore.contextNamespaces);
+ }
+
+ async loadItems(namespaces: string[]) {
+ const isLoadingAll = namespaceStore.allowedNamespaces.every(ns => namespaces.includes(ns));
+ const noAccessibleNamespaces = namespaceStore.context.cluster.accessibleNamespaces.length === 0;
+
+ if (isLoadingAll && noAccessibleNamespaces) {
return helmReleasesApi.list();
} else {
return Promise
@@ -89,7 +93,7 @@ export class ReleaseStore extends ItemStore {
async create(payload: IReleaseCreatePayload) {
const response = await helmReleasesApi.create(payload);
- if (this.isLoaded) this.loadAll();
+ if (this.isLoaded) this.loadFromContextNamespaces();
return response;
}
@@ -97,7 +101,7 @@ export class ReleaseStore extends ItemStore {
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
const response = await helmReleasesApi.update(name, namespace, payload);
- if (this.isLoaded) this.loadAll();
+ if (this.isLoaded) this.loadFromContextNamespaces();
return response;
}
@@ -105,7 +109,7 @@ export class ReleaseStore extends ItemStore {
async rollback(name: string, namespace: string, revision: number) {
const response = await helmReleasesApi.rollback(name, namespace, revision);
- if (this.isLoaded) this.loadAll();
+ if (this.isLoaded) this.loadFromContextNamespaces();
return response;
}
diff --git a/src/renderer/components/+apps-releases/releases.tsx b/src/renderer/components/+apps-releases/releases.tsx
index 709c6f9bbd..71cf3d954f 100644
--- a/src/renderer/components/+apps-releases/releases.tsx
+++ b/src/renderer/components/+apps-releases/releases.tsx
@@ -14,11 +14,13 @@ import { ItemListLayout } from "../item-object-list/item-list-layout";
import { HelmReleaseMenu } from "./release-menu";
import { secretsStore } from "../+config-secrets/secrets.store";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
revision = "revision",
chart = "chart",
+ version = "version",
+ appVersion = "app-version",
status = "status",
updated = "update"
}
@@ -81,16 +83,18 @@ export class HelmReleases extends Component {
return (
<>
release.getName(),
- [sortBy.namespace]: (release: HelmRelease) => release.getNs(),
- [sortBy.revision]: (release: HelmRelease) => release.getRevision(),
- [sortBy.chart]: (release: HelmRelease) => release.getChart(),
- [sortBy.status]: (release: HelmRelease) => release.getStatus(),
- [sortBy.updated]: (release: HelmRelease) => release.getUpdated(false, false),
+ [columnId.name]: (release: HelmRelease) => release.getName(),
+ [columnId.namespace]: (release: HelmRelease) => release.getNs(),
+ [columnId.revision]: (release: HelmRelease) => release.getRevision(),
+ [columnId.chart]: (release: HelmRelease) => release.getChart(),
+ [columnId.status]: (release: HelmRelease) => release.getStatus(),
+ [columnId.updated]: (release: HelmRelease) => release.getUpdated(false, false),
}}
searchFilters={[
(release: HelmRelease) => release.getName(),
@@ -101,14 +105,14 @@ export class HelmReleases extends Component {
]}
renderHeaderTitle="Releases"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Chart", className: "chart", sortBy: sortBy.chart },
- { title: "Revision", className: "revision", sortBy: sortBy.revision },
- { title: "Version", className: "version" },
- { title: "App Version", className: "app-version" },
- { title: "Status", className: "status", sortBy: sortBy.status },
- { title: "Updated", className: "updated", sortBy: sortBy.updated },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Chart", className: "chart", sortBy: columnId.chart, id: columnId.chart },
+ { title: "Revision", className: "revision", sortBy: columnId.revision, id: columnId.revision },
+ { title: "Version", className: "version", id: columnId.version },
+ { title: "App Version", className: "app-version", id: columnId.appVersion },
+ { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
+ { title: "Updated", className: "updated", sortBy: columnId.updated, id: columnId.updated },
]}
renderTableContents={(release: HelmRelease) => {
const version = release.getVersion();
diff --git a/src/renderer/components/+apps/apps.command.ts b/src/renderer/components/+apps/apps.command.ts
new file mode 100644
index 0000000000..ff6c9d615d
--- /dev/null
+++ b/src/renderer/components/+apps/apps.command.ts
@@ -0,0 +1,18 @@
+import { navigate } from "../../navigation";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { helmChartsURL } from "../+apps-helm-charts";
+import { releaseURL } from "../+apps-releases";
+
+commandRegistry.add({
+ id: "cluster.viewHelmCharts",
+ title: "Cluster: View Helm Charts",
+ scope: "cluster",
+ action: () => navigate(helmChartsURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewHelmReleases",
+ title: "Cluster: View Helm Releases",
+ scope: "cluster",
+ action: () => navigate(releaseURL())
+});
diff --git a/src/renderer/components/+apps/index.ts b/src/renderer/components/+apps/index.ts
index 70c0169777..30fbf65316 100644
--- a/src/renderer/components/+apps/index.ts
+++ b/src/renderer/components/+apps/index.ts
@@ -1,2 +1,3 @@
export * from "./apps";
-export * from "./apps.route";
\ No newline at end of file
+export * from "./apps.route";
+export * from "./apps.command";
diff --git a/src/renderer/components/+cluster-settings/cluster-settings.command.ts b/src/renderer/components/+cluster-settings/cluster-settings.command.ts
new file mode 100644
index 0000000000..a3b3c8792e
--- /dev/null
+++ b/src/renderer/components/+cluster-settings/cluster-settings.command.ts
@@ -0,0 +1,16 @@
+import { navigate } from "../../navigation";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { clusterSettingsURL } from "./cluster-settings.route";
+import { clusterStore } from "../../../common/cluster-store";
+
+commandRegistry.add({
+ id: "cluster.viewCurrentClusterSettings",
+ title: "Cluster: View Settings",
+ scope: "global",
+ action: () => navigate(clusterSettingsURL({
+ params: {
+ clusterId: clusterStore.active.id
+ }
+ })),
+ isActive: (context) => !!context.cluster
+});
diff --git a/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx
index 35c18cc5e5..10aabf3ff7 100644
--- a/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx
+++ b/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx
@@ -48,4 +48,4 @@ export class ClusterHomeDirSetting extends React.Component {
>
);
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx
index 631c6d54ef..9d953ef9ca 100644
--- a/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx
+++ b/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx
@@ -45,4 +45,4 @@ export class ClusterNameSetting extends React.Component {
>
);
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx
index 3887487816..eb122ac444 100644
--- a/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx
+++ b/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx
@@ -45,4 +45,4 @@ export class ClusterProxySetting extends React.Component {
>
);
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx
index ea4ee5a571..fa76dde806 100644
--- a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx
+++ b/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx
@@ -1,7 +1,5 @@
import React from "react";
import { observer } from "mobx-react";
-import { Link } from "react-router-dom";
-import { workspacesURL } from "../../+workspaces";
import { workspaceStore } from "../../../../common/workspace-store";
import { Cluster } from "../../../../main/cluster";
import { Select } from "../../../components/select";
@@ -18,10 +16,7 @@ export class ClusterWorkspaceSetting extends React.Component {
<>
- Define cluster{" "}
-
- workspace
- .
+ Define cluster workspace.
{
;
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/+cluster-settings/index.ts b/src/renderer/components/+cluster-settings/index.ts
index edab795e29..b83e440d53 100644
--- a/src/renderer/components/+cluster-settings/index.ts
+++ b/src/renderer/components/+cluster-settings/index.ts
@@ -1,2 +1,3 @@
export * from "./cluster-settings.route";
export * from "./cluster-settings";
+export * from "./cluster-settings.command";
diff --git a/src/renderer/components/+cluster-settings/removal.tsx b/src/renderer/components/+cluster-settings/removal.tsx
index 7d97e9c515..495fb71fe8 100644
--- a/src/renderer/components/+cluster-settings/removal.tsx
+++ b/src/renderer/components/+cluster-settings/removal.tsx
@@ -17,4 +17,4 @@ export class Removal extends React.Component {
);
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/+cluster-settings/status.tsx b/src/renderer/components/+cluster-settings/status.tsx
index 7f21d19aba..d43cfe5c35 100644
--- a/src/renderer/components/+cluster-settings/status.tsx
+++ b/src/renderer/components/+cluster-settings/status.tsx
@@ -58,4 +58,4 @@ export class Status extends React.Component {
;
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/+cluster/cluster-issues.tsx b/src/renderer/components/+cluster/cluster-issues.tsx
index eb85bf79f8..0aabebaa27 100644
--- a/src/renderer/components/+cluster/cluster-issues.tsx
+++ b/src/renderer/components/+cluster/cluster-issues.tsx
@@ -23,11 +23,13 @@ interface IWarning extends ItemObject {
kind: string;
message: string;
selfLink: string;
+ age: string | number;
}
enum sortBy {
type = "type",
- object = "object"
+ object = "object",
+ age = "age",
}
@observer
@@ -35,6 +37,7 @@ export class ClusterIssues extends React.Component {
private sortCallbacks = {
[sortBy.type]: (warning: IWarning) => warning.kind,
[sortBy.object]: (warning: IWarning) => warning.getName(),
+ [sortBy.age]: (warning: IWarning) => warning.age || "",
};
@computed get warnings() {
@@ -42,15 +45,16 @@ export class ClusterIssues extends React.Component {
// Node bad conditions
nodesStore.items.forEach(node => {
- const { kind, selfLink, getId, getName } = node;
+ const { kind, selfLink, getId, getName, getAge } = node;
node.getWarningConditions().forEach(({ message }) => {
warnings.push({
- kind,
+ age: getAge(),
getId,
getName,
- selfLink,
+ kind,
message,
+ selfLink,
});
});
});
@@ -59,12 +63,13 @@ export class ClusterIssues extends React.Component {
const events = eventStore.getWarnings();
events.forEach(error => {
- const { message, involvedObject } = error;
+ const { message, involvedObject, getAge } = error;
const { uid, name, kind } = involvedObject;
warnings.push({
getId: () => uid,
getName: () => name,
+ age: getAge(),
message,
kind,
selfLink: lookupApiLink(involvedObject, error),
@@ -78,7 +83,7 @@ export class ClusterIssues extends React.Component {
getTableRow(uid: string) {
const { warnings } = this;
const warning = warnings.find(warn => warn.getId() == uid);
- const { getId, getName, message, kind, selfLink } = warning;
+ const { getId, getName, message, kind, selfLink, age } = warning;
return (
{
{kind}
+
+ {age}
+
);
}
@@ -139,6 +147,7 @@ export class ClusterIssues extends React.Component {
Message
Object
Type
+ Age
>
diff --git a/src/renderer/components/+cluster/cluster-metrics.tsx b/src/renderer/components/+cluster/cluster-metrics.tsx
index 6461bae7f3..2bdaded8b7 100644
--- a/src/renderer/components/+cluster/cluster-metrics.tsx
+++ b/src/renderer/components/+cluster/cluster-metrics.tsx
@@ -95,4 +95,4 @@ export const ClusterMetrics = observer(() => {
{renderMetrics()}
);
-});
\ No newline at end of file
+});
diff --git a/src/renderer/components/+cluster/cluster-overview.tsx b/src/renderer/components/+cluster/cluster-overview.tsx
index 104c6fd022..bc587671dc 100644
--- a/src/renderer/components/+cluster/cluster-overview.tsx
+++ b/src/renderer/components/+cluster/cluster-overview.tsx
@@ -3,13 +3,9 @@ import "./cluster-overview.scss";
import React from "react";
import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
-
-import { eventStore } from "../+events/event.store";
import { nodesStore } from "../+nodes/nodes.store";
import { podsStore } from "../+workloads-pods/pods.store";
import { getHostedCluster } from "../../../common/cluster-store";
-import { isAllowedResource } from "../../../common/rbac";
-import { KubeObjectStore } from "../../kube-object.store";
import { interval } from "../../utils";
import { TabLayout } from "../layout/tab-layout";
import { Spinner } from "../spinner";
@@ -20,42 +16,24 @@ import { ClusterPieCharts } from "./cluster-pie-charts";
@observer
export class ClusterOverview extends React.Component {
- private stores: KubeObjectStore[] = [];
- private subscribers: Array<() => void> = [];
- private metricPoller = interval(60, this.loadMetrics);
-
- @disposeOnUnmount
- fetchMetrics = reaction(
- () => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
- () => this.metricPoller.restart(true)
- );
+ private metricPoller = interval(60, () => this.loadMetrics());
loadMetrics() {
getHostedCluster().available && clusterOverviewStore.loadMetrics();
}
- async componentDidMount() {
- if (isAllowedResource("nodes")) {
- this.stores.push(nodesStore);
- }
+ componentDidMount() {
+ this.metricPoller.start(true);
- if (isAllowedResource("pods")) {
- this.stores.push(podsStore);
- }
-
- if (isAllowedResource("events")) {
- this.stores.push(eventStore);
- }
-
- await Promise.all(this.stores.map(store => store.loadAll()));
- this.loadMetrics();
-
- this.subscribers = this.stores.map(store => store.subscribe());
- this.metricPoller.start();
+ disposeOnUnmount(this, [
+ reaction(
+ () => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
+ () => this.metricPoller.restart(true)
+ ),
+ ]);
}
componentWillUnmount() {
- this.subscribers.forEach(dispose => dispose()); // unsubscribe all
this.metricPoller.stop();
}
diff --git a/src/renderer/components/+cluster/cluster-pie-charts.tsx b/src/renderer/components/+cluster/cluster-pie-charts.tsx
index 1881d5dcce..d64efb6b36 100644
--- a/src/renderer/components/+cluster/cluster-pie-charts.tsx
+++ b/src/renderer/components/+cluster/cluster-pie-charts.tsx
@@ -143,7 +143,7 @@ export const ClusterPieCharts = observer(() => {
{cpuLimitsOverload && renderLimitWarning()}
@@ -151,7 +151,7 @@ export const ClusterPieCharts = observer(() => {
{memoryLimitsOverload && renderLimitWarning()}
@@ -159,7 +159,7 @@ export const ClusterPieCharts = observer(() => {
diff --git a/src/renderer/components/+config-autoscalers/hpa.tsx b/src/renderer/components/+config-autoscalers/hpa.tsx
index 023a28f156..2e2a78fc82 100644
--- a/src/renderer/components/+config-autoscalers/hpa.tsx
+++ b/src/renderer/components/+config-autoscalers/hpa.tsx
@@ -11,13 +11,15 @@ import { Badge } from "../badge";
import { cssNames } from "../../utils";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
+ metrics = "metrics",
minPods = "min-pods",
maxPods = "max-pods",
replicas = "replicas",
age = "age",
+ status = "status"
}
interface Props extends RouteComponentProps
{
@@ -37,28 +39,30 @@ export class HorizontalPodAutoscalers extends React.Component {
render() {
return (
item.getName(),
- [sortBy.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(),
- [sortBy.minPods]: (item: HorizontalPodAutoscaler) => item.getMinPods(),
- [sortBy.maxPods]: (item: HorizontalPodAutoscaler) => item.getMaxPods(),
- [sortBy.replicas]: (item: HorizontalPodAutoscaler) => item.getReplicas()
+ [columnId.name]: (item: HorizontalPodAutoscaler) => item.getName(),
+ [columnId.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(),
+ [columnId.minPods]: (item: HorizontalPodAutoscaler) => item.getMinPods(),
+ [columnId.maxPods]: (item: HorizontalPodAutoscaler) => item.getMaxPods(),
+ [columnId.replicas]: (item: HorizontalPodAutoscaler) => item.getReplicas()
}}
searchFilters={[
(item: HorizontalPodAutoscaler) => item.getSearchFields()
]}
renderHeaderTitle="Horizontal Pod Autoscalers"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Metrics", className: "metrics" },
- { title: "Min Pods", className: "min-pods", sortBy: sortBy.minPods },
- { title: "Max Pods", className: "max-pods", sortBy: sortBy.maxPods },
- { title: "Replicas", className: "replicas", sortBy: sortBy.replicas },
- { title: "Age", className: "age", sortBy: sortBy.age },
- { title: "Status", className: "status" },
+ { title: "Name", className: "name", sortBy: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Metrics", className: "metrics", id: columnId.metrics },
+ { title: "Min Pods", className: "min-pods", sortBy: columnId.minPods, id: columnId.minPods },
+ { title: "Max Pods", className: "max-pods", sortBy: columnId.maxPods, id: columnId.maxPods },
+ { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
+ { title: "Status", className: "status", id: columnId.status },
]}
renderTableContents={(hpa: HorizontalPodAutoscaler) => [
hpa.getName(),
diff --git a/src/renderer/components/+config-limit-ranges/index.ts b/src/renderer/components/+config-limit-ranges/index.ts
new file mode 100644
index 0000000000..53308bdd18
--- /dev/null
+++ b/src/renderer/components/+config-limit-ranges/index.ts
@@ -0,0 +1,3 @@
+export * from "./limit-ranges";
+export * from "./limit-ranges.route";
+export * from "./limit-range-details";
diff --git a/src/renderer/components/+config-limit-ranges/limit-range-details.scss b/src/renderer/components/+config-limit-ranges/limit-range-details.scss
new file mode 100644
index 0000000000..ff39ec514a
--- /dev/null
+++ b/src/renderer/components/+config-limit-ranges/limit-range-details.scss
@@ -0,0 +1,12 @@
+.LimitRangeDetails {
+
+ .DrawerItem {
+ > .name {
+ font-weight: $font-weight-normal;
+ padding-left: 4px;
+ }
+ .DrawerItem {
+ padding-top: 4px;
+ }
+ }
+}
diff --git a/src/renderer/components/+config-limit-ranges/limit-range-details.tsx b/src/renderer/components/+config-limit-ranges/limit-range-details.tsx
new file mode 100644
index 0000000000..105ccc41dd
--- /dev/null
+++ b/src/renderer/components/+config-limit-ranges/limit-range-details.tsx
@@ -0,0 +1,97 @@
+import "./limit-range-details.scss";
+
+import React from "react";
+import { observer } from "mobx-react";
+import { KubeObjectDetailsProps } from "../kube-object";
+import { LimitPart, LimitRange, LimitRangeItem, Resource } from "../../api/endpoints/limit-range.api";
+import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
+import { KubeObjectMeta } from "../kube-object/kube-object-meta";
+import { DrawerItem } from "../drawer/drawer-item";
+import { Badge } from "../badge";
+
+interface Props extends KubeObjectDetailsProps {
+}
+
+function renderLimit(limit: LimitRangeItem, part: LimitPart, resource: Resource) {
+
+ const resourceLimit = limit[part]?.[resource];
+
+ if (!resourceLimit) {
+ return null;
+ }
+
+ return ;
+}
+
+function renderResourceLimits(limit: LimitRangeItem, resource: Resource) {
+ return (
+
+ {renderLimit(limit, LimitPart.MIN, resource)}
+ {renderLimit(limit, LimitPart.MAX, resource)}
+ {renderLimit(limit, LimitPart.DEFAULT, resource)}
+ {renderLimit(limit, LimitPart.DEFAULT_REQUEST, resource)}
+ {renderLimit(limit, LimitPart.MAX_LIMIT_REQUEST_RATIO, resource)}
+
+ );
+}
+
+function renderLimitDetails(limits: LimitRangeItem[], resources: Resource[]) {
+
+ return resources.map(resource =>
+
+ {
+ limits.map(limit =>
+ renderResourceLimits(limit, resource)
+ )
+ }
+
+ );
+}
+
+@observer
+export class LimitRangeDetails extends React.Component {
+ render() {
+ const { object: limitRange } = this.props;
+
+ if (!limitRange) return null;
+ const containerLimits = limitRange.getContainerLimits();
+ const podLimits = limitRange.getPodLimits();
+ const pvcLimits = limitRange.getPVCLimits();
+
+ return (
+
+
+
+ {containerLimits.length > 0 &&
+
+ {
+ renderLimitDetails(containerLimits, [Resource.CPU, Resource.MEMORY, Resource.EPHEMERAL_STORAGE])
+ }
+
+ }
+ {podLimits.length > 0 &&
+
+ {
+ renderLimitDetails(podLimits, [Resource.CPU, Resource.MEMORY, Resource.EPHEMERAL_STORAGE])
+ }
+
+ }
+ {pvcLimits.length > 0 &&
+
+ {
+ renderLimitDetails(pvcLimits, [Resource.STORAGE])
+ }
+
+ }
+
+ );
+ }
+}
+
+kubeObjectDetailRegistry.add({
+ kind: "LimitRange",
+ apiVersions: ["v1"],
+ components: {
+ Details: (props) =>
+ }
+});
diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges.route.ts b/src/renderer/components/+config-limit-ranges/limit-ranges.route.ts
new file mode 100644
index 0000000000..09e3052350
--- /dev/null
+++ b/src/renderer/components/+config-limit-ranges/limit-ranges.route.ts
@@ -0,0 +1,11 @@
+import type { RouteProps } from "react-router";
+import { buildURL } from "../../../common/utils/buildUrl";
+
+export const limitRangesRoute: RouteProps = {
+ path: "/limitranges"
+};
+
+export interface LimitRangeRouteParams {
+}
+
+export const limitRangeURL = buildURL(limitRangesRoute.path);
diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges.scss b/src/renderer/components/+config-limit-ranges/limit-ranges.scss
new file mode 100644
index 0000000000..e5de19acfb
--- /dev/null
+++ b/src/renderer/components/+config-limit-ranges/limit-ranges.scss
@@ -0,0 +1,7 @@
+.LimitRanges {
+ .TableCell {
+ &.warning {
+ @include table-cell-warning;
+ }
+ }
+}
diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges.store.ts b/src/renderer/components/+config-limit-ranges/limit-ranges.store.ts
new file mode 100644
index 0000000000..bd760efadd
--- /dev/null
+++ b/src/renderer/components/+config-limit-ranges/limit-ranges.store.ts
@@ -0,0 +1,12 @@
+import { autobind } from "../../../common/utils/autobind";
+import { KubeObjectStore } from "../../kube-object.store";
+import { apiManager } from "../../api/api-manager";
+import { LimitRange, limitRangeApi } from "../../api/endpoints/limit-range.api";
+
+@autobind()
+export class LimitRangesStore extends KubeObjectStore {
+ api = limitRangeApi;
+}
+
+export const limitRangeStore = new LimitRangesStore();
+apiManager.registerStore(limitRangeStore);
diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges.tsx b/src/renderer/components/+config-limit-ranges/limit-ranges.tsx
new file mode 100644
index 0000000000..715bc4430b
--- /dev/null
+++ b/src/renderer/components/+config-limit-ranges/limit-ranges.tsx
@@ -0,0 +1,55 @@
+import "./limit-ranges.scss";
+
+import { RouteComponentProps } from "react-router";
+import { observer } from "mobx-react";
+import { KubeObjectListLayout } from "../kube-object/kube-object-list-layout";
+import { limitRangeStore } from "./limit-ranges.store";
+import { LimitRangeRouteParams } from "./limit-ranges.route";
+import React from "react";
+import { KubeObjectStatusIcon } from "../kube-object-status-icon";
+import { LimitRange } from "../../api/endpoints/limit-range.api";
+
+enum columnId {
+ name = "name",
+ namespace = "namespace",
+ age = "age",
+}
+
+interface Props extends RouteComponentProps {
+}
+
+@observer
+export class LimitRanges extends React.Component {
+ render() {
+ return (
+ item.getName(),
+ [columnId.namespace]: (item: LimitRange) => item.getNs(),
+ [columnId.age]: (item: LimitRange) => item.getTimeDiffFromNow(),
+ }}
+ searchFilters={[
+ (item: LimitRange) => item.getName(),
+ (item: LimitRange) => item.getNs(),
+ ]}
+ renderHeaderTitle={"Limit Ranges"}
+ renderTableHeader={[
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
+ ]}
+ renderTableContents={(limitRange: LimitRange) => [
+ limitRange.getName(),
+ ,
+ limitRange.getNs(),
+ limitRange.getAge(),
+ ]}
+ />
+ );
+ }
+}
diff --git a/src/renderer/components/+config-maps/config-maps.tsx b/src/renderer/components/+config-maps/config-maps.tsx
index 128c583fc8..4d68319147 100644
--- a/src/renderer/components/+config-maps/config-maps.tsx
+++ b/src/renderer/components/+config-maps/config-maps.tsx
@@ -9,7 +9,7 @@ import { KubeObjectListLayout } from "../kube-object";
import { IConfigMapsRouteParams } from "./config-maps.route";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
keys = "keys",
@@ -24,12 +24,14 @@ export class ConfigMaps extends React.Component {
render() {
return (
item.getName(),
- [sortBy.namespace]: (item: ConfigMap) => item.getNs(),
- [sortBy.keys]: (item: ConfigMap) => item.getKeys(),
- [sortBy.age]: (item: ConfigMap) => item.metadata.creationTimestamp,
+ [columnId.name]: (item: ConfigMap) => item.getName(),
+ [columnId.namespace]: (item: ConfigMap) => item.getNs(),
+ [columnId.keys]: (item: ConfigMap) => item.getKeys(),
+ [columnId.age]: (item: ConfigMap) => item.getTimeDiffFromNow(),
}}
searchFilters={[
(item: ConfigMap) => item.getSearchFields(),
@@ -37,11 +39,11 @@ export class ConfigMaps extends React.Component {
]}
renderHeaderTitle="Config Maps"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Keys", className: "keys", sortBy: sortBy.keys },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Keys", className: "keys", sortBy: columnId.keys, id: columnId.keys },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(configMap: ConfigMap) => [
configMap.getName(),
diff --git a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx
index f0754e0be8..8136225f11 100644
--- a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx
+++ b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx
@@ -7,7 +7,7 @@ import { PodDisruptionBudget } from "../../api/endpoints/poddisruptionbudget.api
import { KubeObjectDetailsProps, KubeObjectListLayout } from "../kube-object";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
minAvailable = "min-available",
@@ -25,30 +25,32 @@ export class PodDisruptionBudgets extends React.Component {
render() {
return (
pdb.getName(),
- [sortBy.namespace]: (pdb: PodDisruptionBudget) => pdb.getNs(),
- [sortBy.minAvailable]: (pdb: PodDisruptionBudget) => pdb.getMinAvailable(),
- [sortBy.maxUnavailable]: (pdb: PodDisruptionBudget) => pdb.getMaxUnavailable(),
- [sortBy.currentHealthy]: (pdb: PodDisruptionBudget) => pdb.getCurrentHealthy(),
- [sortBy.desiredHealthy]: (pdb: PodDisruptionBudget) => pdb.getDesiredHealthy(),
- [sortBy.age]: (pdb: PodDisruptionBudget) => pdb.getAge(),
+ [columnId.name]: (pdb: PodDisruptionBudget) => pdb.getName(),
+ [columnId.namespace]: (pdb: PodDisruptionBudget) => pdb.getNs(),
+ [columnId.minAvailable]: (pdb: PodDisruptionBudget) => pdb.getMinAvailable(),
+ [columnId.maxUnavailable]: (pdb: PodDisruptionBudget) => pdb.getMaxUnavailable(),
+ [columnId.currentHealthy]: (pdb: PodDisruptionBudget) => pdb.getCurrentHealthy(),
+ [columnId.desiredHealthy]: (pdb: PodDisruptionBudget) => pdb.getDesiredHealthy(),
+ [columnId.age]: (pdb: PodDisruptionBudget) => pdb.getAge(),
}}
searchFilters={[
(pdb: PodDisruptionBudget) => pdb.getSearchFields(),
]}
renderHeaderTitle="Pod Disruption Budgets"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Min Available", className: "min-available", sortBy: sortBy.minAvailable },
- { title: "Max Unavailable", className: "max-unavailable", sortBy: sortBy.maxUnavailable },
- { title: "Current Healthy", className: "current-healthy", sortBy: sortBy.currentHealthy },
- { title: "Desired Healthy", className: "desired-healthy", sortBy: sortBy.desiredHealthy },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Min Available", className: "min-available", sortBy: columnId.minAvailable, id: columnId.minAvailable },
+ { title: "Max Unavailable", className: "max-unavailable", sortBy: columnId.maxUnavailable, id: columnId.maxUnavailable },
+ { title: "Current Healthy", className: "current-healthy", sortBy: columnId.currentHealthy, id: columnId.currentHealthy },
+ { title: "Desired Healthy", className: "desired-healthy", sortBy: columnId.desiredHealthy, id: columnId.desiredHealthy },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(pdb: PodDisruptionBudget) => {
return [
diff --git a/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx b/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx
index 032fa6eef2..95f50c935b 100644
--- a/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx
+++ b/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx
@@ -146,7 +146,7 @@ export class AddQuotaDialog extends React.Component {
diff --git a/src/renderer/components/+config-resource-quotas/resource-quotas.tsx b/src/renderer/components/+config-resource-quotas/resource-quotas.tsx
index 1aed2c9d24..3414b74d6a 100644
--- a/src/renderer/components/+config-resource-quotas/resource-quotas.tsx
+++ b/src/renderer/components/+config-resource-quotas/resource-quotas.tsx
@@ -10,7 +10,7 @@ import { resourceQuotaStore } from "./resource-quotas.store";
import { IResourceQuotaRouteParams } from "./resource-quotas.route";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
age = "age"
@@ -25,11 +25,13 @@ export class ResourceQuotas extends React.Component {
return (
<>
item.getName(),
- [sortBy.namespace]: (item: ResourceQuota) => item.getNs(),
- [sortBy.age]: (item: ResourceQuota) => item.metadata.creationTimestamp,
+ [columnId.name]: (item: ResourceQuota) => item.getName(),
+ [columnId.namespace]: (item: ResourceQuota) => item.getNs(),
+ [columnId.age]: (item: ResourceQuota) => item.getTimeDiffFromNow(),
}}
searchFilters={[
(item: ResourceQuota) => item.getSearchFields(),
@@ -37,10 +39,10 @@ export class ResourceQuotas extends React.Component {
]}
renderHeaderTitle="Resource Quotas"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(resourceQuota: ResourceQuota) => [
resourceQuota.getName(),
diff --git a/src/renderer/components/+config-secrets/add-secret-dialog.tsx b/src/renderer/components/+config-secrets/add-secret-dialog.tsx
index 5071c908bf..2fae17d30d 100644
--- a/src/renderer/components/+config-secrets/add-secret-dialog.tsx
+++ b/src/renderer/components/+config-secrets/add-secret-dialog.tsx
@@ -133,7 +133,7 @@ export class AddSecretDialog extends React.Component {
this.addField(field)}
/>
@@ -146,7 +146,7 @@ export class AddSecretDialog extends React.Component {
{
multiLine maxRows={5}
required={required}
className="value"
- placeholder={`Value`}
+ placeholder="Value"
value={value} onChange={v => item.value = v}
/>
{
this.name = v}
/>
diff --git a/src/renderer/components/+config-secrets/secret-details.tsx b/src/renderer/components/+config-secrets/secret-details.tsx
index 92a58141ac..ab7bc59e46 100644
--- a/src/renderer/components/+config-secrets/secret-details.tsx
+++ b/src/renderer/components/+config-secrets/secret-details.tsx
@@ -69,7 +69,7 @@ export class SecretDetails extends React.Component {
{!isEmpty(this.data) && (
<>
-
+
{
Object.entries(this.data).map(([name, value]) => {
const revealSecret = this.revealSecret[name];
@@ -107,7 +107,7 @@ export class SecretDetails extends React.Component {
}
diff --git a/src/renderer/components/+config-secrets/secrets.tsx b/src/renderer/components/+config-secrets/secrets.tsx
index f2c88fda58..e660db3a09 100644
--- a/src/renderer/components/+config-secrets/secrets.tsx
+++ b/src/renderer/components/+config-secrets/secrets.tsx
@@ -11,7 +11,7 @@ import { Badge } from "../badge";
import { secretsStore } from "./secrets.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
labels = "labels",
@@ -29,14 +29,16 @@ export class Secrets extends React.Component {
return (
<>
item.getName(),
- [sortBy.namespace]: (item: Secret) => item.getNs(),
- [sortBy.labels]: (item: Secret) => item.getLabels(),
- [sortBy.keys]: (item: Secret) => item.getKeys(),
- [sortBy.type]: (item: Secret) => item.type,
- [sortBy.age]: (item: Secret) => item.metadata.creationTimestamp,
+ [columnId.name]: (item: Secret) => item.getName(),
+ [columnId.namespace]: (item: Secret) => item.getNs(),
+ [columnId.labels]: (item: Secret) => item.getLabels(),
+ [columnId.keys]: (item: Secret) => item.getKeys(),
+ [columnId.type]: (item: Secret) => item.type,
+ [columnId.age]: (item: Secret) => item.getTimeDiffFromNow(),
}}
searchFilters={[
(item: Secret) => item.getSearchFields(),
@@ -44,13 +46,13 @@ export class Secrets extends React.Component {
]}
renderHeaderTitle="Secrets"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Labels", className: "labels", sortBy: sortBy.labels },
- { title: "Keys", className: "keys", sortBy: sortBy.keys },
- { title: "Type", className: "type", sortBy: sortBy.type },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Labels", className: "labels", sortBy: columnId.labels, id: columnId.labels },
+ { title: "Keys", className: "keys", sortBy: columnId.keys, id: columnId.keys },
+ { title: "Type", className: "type", sortBy: columnId.type, id: columnId.type },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(secret: Secret) => [
secret.getName(),
diff --git a/src/renderer/components/+config/config.command.ts b/src/renderer/components/+config/config.command.ts
new file mode 100644
index 0000000000..b709e6e9cd
--- /dev/null
+++ b/src/renderer/components/+config/config.command.ts
@@ -0,0 +1,50 @@
+import { navigate } from "../../navigation";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { configMapsURL } from "../+config-maps";
+import { secretsURL } from "../+config-secrets";
+import { resourceQuotaURL } from "../+config-resource-quotas";
+import { limitRangeURL } from "../+config-limit-ranges";
+import { hpaURL } from "../+config-autoscalers";
+import { pdbURL } from "../+config-pod-disruption-budgets";
+
+commandRegistry.add({
+ id: "cluster.viewConfigMaps",
+ title: "Cluster: View ConfigMaps",
+ scope: "cluster",
+ action: () => navigate(configMapsURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewSecrets",
+ title: "Cluster: View Secrets",
+ scope: "cluster",
+ action: () => navigate(secretsURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewResourceQuotas",
+ title: "Cluster: View ResourceQuotas",
+ scope: "cluster",
+ action: () => navigate(resourceQuotaURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewLimitRanges",
+ title: "Cluster: View LimitRanges",
+ scope: "cluster",
+ action: () => navigate(limitRangeURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewHorizontalPodAutoscalers",
+ title: "Cluster: View HorizontalPodAutoscalers (HPA)",
+ scope: "cluster",
+ action: () => navigate(hpaURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewPodDisruptionBudget",
+ title: "Cluster: View PodDisruptionBudgets",
+ scope: "cluster",
+ action: () => navigate(pdbURL())
+});
diff --git a/src/renderer/components/+config/config.tsx b/src/renderer/components/+config/config.tsx
index bb70dd3fb5..e3158459ba 100644
--- a/src/renderer/components/+config/config.tsx
+++ b/src/renderer/components/+config/config.tsx
@@ -8,6 +8,7 @@ import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config
import { pdbRoute, pdbURL, PodDisruptionBudgets } from "../+config-pod-disruption-budgets";
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
import { isAllowedResource } from "../../../common/rbac";
+import { LimitRanges, limitRangesRoute, limitRangeURL } from "../+config-limit-ranges";
@observer
export class Config extends React.Component {
@@ -42,6 +43,15 @@ export class Config extends React.Component {
});
}
+ if (isAllowedResource("limitranges")) {
+ routes.push({
+ title: "Limit Ranges",
+ component: LimitRanges,
+ url: limitRangeURL({ query }),
+ routePath: limitRangesRoute.path.toString(),
+ });
+ }
+
if (isAllowedResource("horizontalpodautoscalers")) {
routes.push({
title: "HPA",
diff --git a/src/renderer/components/+config/index.ts b/src/renderer/components/+config/index.ts
index dba36b66d6..bfc04ecbe6 100644
--- a/src/renderer/components/+config/index.ts
+++ b/src/renderer/components/+config/index.ts
@@ -1,2 +1,3 @@
export * from "./config.route";
export * from "./config";
+export * from "./config.command";
diff --git a/src/renderer/components/+custom-resources/crd-list.tsx b/src/renderer/components/+custom-resources/crd-list.tsx
index 8868231235..f8b77c09a9 100644
--- a/src/renderer/components/+custom-resources/crd-list.tsx
+++ b/src/renderer/components/+custom-resources/crd-list.tsx
@@ -19,7 +19,7 @@ export const crdGroupsUrlParam = createPageParam({
defaultValue: [],
});
-enum sortBy {
+enum columnId {
kind = "kind",
group = "group",
version = "version",
@@ -47,14 +47,16 @@ export class CrdList extends React.Component {
render() {
const selectedGroups = this.groups;
const sortingCallbacks = {
- [sortBy.kind]: (crd: CustomResourceDefinition) => crd.getResourceKind(),
- [sortBy.group]: (crd: CustomResourceDefinition) => crd.getGroup(),
- [sortBy.version]: (crd: CustomResourceDefinition) => crd.getVersion(),
- [sortBy.scope]: (crd: CustomResourceDefinition) => crd.getScope(),
+ [columnId.kind]: (crd: CustomResourceDefinition) => crd.getResourceKind(),
+ [columnId.group]: (crd: CustomResourceDefinition) => crd.getGroup(),
+ [columnId.version]: (crd: CustomResourceDefinition) => crd.getVersion(),
+ [columnId.scope]: (crd: CustomResourceDefinition) => crd.getScope(),
};
return (
[
diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx
index e6a7f2aac6..c60a93f063 100644
--- a/src/renderer/components/+custom-resources/crd-resources.tsx
+++ b/src/renderer/components/+custom-resources/crd-resources.tsx
@@ -16,7 +16,7 @@ import { parseJsonPath } from "../../utils/jsonPath";
interface Props extends RouteComponentProps {
}
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
age = "age",
@@ -30,7 +30,7 @@ export class CrdResources extends React.Component {
const { store } = this;
if (store && !store.isLoading && !store.isLoaded) {
- store.loadAll();
+ store.reloadAll();
}
})
]);
@@ -55,9 +55,9 @@ export class CrdResources extends React.Component {
const isNamespaced = crd.isNamespaced();
const extraColumns = crd.getPrinterColumns(false); // Cols with priority bigger than 0 are shown in details
const sortingCallbacks: { [sortBy: string]: TableSortCallback } = {
- [sortBy.name]: (item: KubeObject) => item.getName(),
- [sortBy.namespace]: (item: KubeObject) => item.getNs(),
- [sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp,
+ [columnId.name]: (item: KubeObject) => item.getName(),
+ [columnId.namespace]: (item: KubeObject) => item.getNs(),
+ [columnId.age]: (item: KubeObject) => item.getTimeDiffFromNow(),
};
extraColumns.forEach(column => {
@@ -66,6 +66,8 @@ export class CrdResources extends React.Component {
return (
{
]}
renderHeaderTitle={crd.getResourceTitle()}
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- isNamespaced && { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ isNamespaced && { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
...extraColumns.map(column => {
const { name } = column;
return {
title: name,
className: name.toLowerCase(),
- sortBy: name
+ sortBy: name,
+ id: name
};
}),
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(crdInstance: KubeObject) => [
crdInstance.getName(),
@@ -94,7 +97,7 @@ export class CrdResources extends React.Component {
...extraColumns.map((column) => {
let value = jsonPath.value(crdInstance, parseJsonPath(column.jsonPath.slice(1)));
- if (Array.isArray(value) ||Ā typeof value === "object") {
+ if (Array.isArray(value) || typeof value === "object") {
value = JSON.stringify(value);
}
diff --git a/src/renderer/components/+events/event.store.ts b/src/renderer/components/+events/event.store.ts
index 3651ce1549..03d272643f 100644
--- a/src/renderer/components/+events/event.store.ts
+++ b/src/renderer/components/+events/event.store.ts
@@ -20,8 +20,8 @@ export class EventStore extends KubeObjectStore {
protected sortItems(items: KubeEvent[]) {
return super.sortItems(items, [
- event => event.metadata.creationTimestamp
- ], "desc");
+ event => event.getTimeDiffFromNow(), // keep events order as timeline ("fresh" on top)
+ ], "asc");
}
getEventsByObject(obj: KubeObject): KubeEvent[] {
@@ -52,6 +52,10 @@ export class EventStore extends KubeObjectStore {
return compact(eventsWithError);
}
+
+ getWarningsCount() {
+ return this.getWarnings().length;
+ }
}
export const eventStore = new EventStore();
diff --git a/src/renderer/components/+events/events.tsx b/src/renderer/components/+events/events.tsx
index c4e6920bc8..4b7b64dd15 100644
--- a/src/renderer/components/+events/events.tsx
+++ b/src/renderer/components/+events/events.tsx
@@ -1,22 +1,29 @@
import "./events.scss";
import React, { Fragment } from "react";
+import { computed, observable } from "mobx";
import { observer } from "mobx-react";
+import { orderBy } from "lodash";
import { TabLayout } from "../layout/tab-layout";
-import { eventStore } from "./event.store";
+import { EventStore, eventStore } from "./event.store";
import { getDetailsUrl, KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
import { KubeEvent } from "../../api/endpoints/events.api";
+import { TableSortCallbacks, TableSortParams, TableProps } from "../table";
+import { IHeaderPlaceholders } from "../item-object-list";
import { Tooltip } from "../tooltip";
import { Link } from "react-router-dom";
import { cssNames, IClassName, stopPropagation } from "../../utils";
import { Icon } from "../icon";
import { lookupApiLink } from "../../api/kube-api";
+import { eventsURL } from "./events.route";
-enum sortBy {
+enum columnId {
+ message = "message",
namespace = "namespace",
object = "object",
type = "type",
count = "count",
+ source = "source",
age = "age",
}
@@ -34,60 +41,116 @@ const defaultProps: Partial = {
export class Events extends React.Component {
static defaultProps = defaultProps as object;
+ @observable sorting: TableSortParams = {
+ sortBy: columnId.age,
+ orderBy: "asc",
+ };
+
+ private sortingCallbacks: TableSortCallbacks = {
+ [columnId.namespace]: (event: KubeEvent) => event.getNs(),
+ [columnId.type]: (event: KubeEvent) => event.type,
+ [columnId.object]: (event: KubeEvent) => event.involvedObject.name,
+ [columnId.count]: (event: KubeEvent) => event.count,
+ [columnId.age]: (event: KubeEvent) => event.getTimeDiffFromNow(),
+ };
+
+ private tableConfiguration: TableProps = {
+ sortSyncWithUrl: false,
+ sortByDefault: this.sorting,
+ onSort: params => this.sorting = params,
+ };
+
+ get store(): EventStore {
+ return eventStore;
+ }
+
+ @computed get items(): KubeEvent[] {
+ const items = this.store.contextItems;
+ const { sortBy, orderBy: order } = this.sorting;
+
+ // we must sort items before passing to "KubeObjectListLayout -> Table"
+ // to make it work with "compact=true" (proper table sorting actions + initial items)
+ return orderBy(items, this.sortingCallbacks[sortBy], order as any);
+ }
+
+ @computed get visibleItems(): KubeEvent[] {
+ const { compact, compactLimit } = this.props;
+
+ if (compact) {
+ return this.items.slice(0, compactLimit);
+ }
+
+ return this.items;
+ }
+
+ customizeHeader = ({ info, title }: IHeaderPlaceholders) => {
+ const { compact } = this.props;
+ const { store, items, visibleItems } = this;
+ const allEventsAreShown = visibleItems.length === items.length;
+
+ // handle "compact"-mode header
+ if (compact) {
+ if (allEventsAreShown) return title; // title == "Events"
+
+ return <>
+ {title}
+ ({visibleItems.length} of {items.length})
+ >;
+ }
+
+ return {
+ info: <>
+ {info}
+
+ >
+ };
+ };
+
render() {
+ const { store, visibleItems } = this;
const { compact, compactLimit, className, ...layoutProps } = this.props;
+
const events = (
event.getNs(),
- [sortBy.type]: (event: KubeEvent) => event.involvedObject.kind,
- [sortBy.object]: (event: KubeEvent) => event.involvedObject.name,
- [sortBy.count]: (event: KubeEvent) => event.count,
- [sortBy.age]: (event: KubeEvent) => event.metadata.creationTimestamp,
- }}
+ items={visibleItems}
+ virtual={!compact}
+ tableProps={this.tableConfiguration}
+ sortingCallbacks={this.sortingCallbacks}
searchFilters={[
(event: KubeEvent) => event.getSearchFields(),
(event: KubeEvent) => event.message,
(event: KubeEvent) => event.getSource(),
(event: KubeEvent) => event.involvedObject.name,
]}
- renderHeaderTitle="Events"
- customizeHeader={({ title, info }) => (
- compact ? title : ({
- info: (
- <>
- {info}
-
- >
- )
- })
- )}
renderTableHeader={[
- { title: "Message", className: "message" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Type", className: "type", sortBy: sortBy.type },
- { title: "Involved Object", className: "object", sortBy: sortBy.object },
- { title: "Source", className: "source" },
- { title: "Count", className: "count", sortBy: sortBy.count },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Type", className: "type", sortBy: columnId.type, id: columnId.type },
+ { title: "Message", className: "message", id: columnId.message },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Involved Object", className: "object", sortBy: columnId.object, id: columnId.object },
+ { title: "Source", className: "source", id: columnId.source },
+ { title: "Count", className: "count", sortBy: columnId.count, id: columnId.count },
+ { title: "Last Seen", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(event: KubeEvent) => {
const { involvedObject, type, message } = event;
- const { kind, name } = involvedObject;
const tooltipId = `message-${event.getId()}`;
- const isWarning = type === "Warning";
- const detailsUrl = getDetailsUrl(lookupApiLink(involvedObject, event));
+ const isWarning = event.isWarning();
return [
+ type, // type of event: "Normal" or "Warning"
{
className: { warning: isWarning },
title: (
@@ -100,17 +163,14 @@ export class Events extends React.Component {
)
},
event.getNs(),
- kind,
- {name},
+
+ {involvedObject.kind}: {involvedObject.name}
+ ,
event.getSource(),
event.count,
event.getAge(),
];
}}
- virtual={!compact}
- filterItems={[
- items => compact ? items.slice(0, compactLimit) : items,
- ]}
/>
);
diff --git a/src/renderer/components/+events/kube-event-details.tsx b/src/renderer/components/+events/kube-event-details.tsx
index 34b16103f0..264b99f11b 100644
--- a/src/renderer/components/+events/kube-event-details.tsx
+++ b/src/renderer/components/+events/kube-event-details.tsx
@@ -14,7 +14,7 @@ export interface KubeEventDetailsProps {
@observer
export class KubeEventDetails extends React.Component {
async componentDidMount() {
- eventStore.loadAll();
+ eventStore.reloadAll();
}
render() {
diff --git a/src/renderer/components/+landing-page/index.tsx b/src/renderer/components/+landing-page/index.tsx
index 4bdb2a706c..c7eacf1bd0 100644
--- a/src/renderer/components/+landing-page/index.tsx
+++ b/src/renderer/components/+landing-page/index.tsx
@@ -1,2 +1,2 @@
export * from "./landing-page.route";
-export * from "./landing-page";
\ No newline at end of file
+export * from "./landing-page";
diff --git a/src/renderer/components/+namespaces/add-namespace-dialog.tsx b/src/renderer/components/+namespaces/add-namespace-dialog.tsx
index 16730afce9..a3b30235fa 100644
--- a/src/renderer/components/+namespaces/add-namespace-dialog.tsx
+++ b/src/renderer/components/+namespaces/add-namespace-dialog.tsx
@@ -72,7 +72,7 @@ export class AddNamespaceDialog extends React.Component {
this.namespace = v.toLowerCase()}
/>
diff --git a/src/renderer/components/+namespaces/namespace-details.tsx b/src/renderer/components/+namespaces/namespace-details.tsx
index f687630a96..d43b348ce5 100644
--- a/src/renderer/components/+namespaces/namespace-details.tsx
+++ b/src/renderer/components/+namespaces/namespace-details.tsx
@@ -12,6 +12,7 @@ import { Spinner } from "../spinner";
import { resourceQuotaStore } from "../+config-resource-quotas/resource-quotas.store";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
+import { limitRangeStore } from "../+config-limit-ranges/limit-ranges.store";
interface Props extends KubeObjectDetailsProps {
}
@@ -24,8 +25,15 @@ export class NamespaceDetails extends React.Component {
return resourceQuotaStore.getAllByNs(namespace);
}
+ @computed get limitranges() {
+ const namespace = this.props.object.getName();
+
+ return limitRangeStore.getAllByNs(namespace);
+ }
+
componentDidMount() {
- resourceQuotaStore.loadAll();
+ resourceQuotaStore.reloadAll();
+ limitRangeStore.reloadAll();
}
render() {
@@ -52,6 +60,16 @@ export class NamespaceDetails extends React.Component {
);
})}
+
+ {!this.limitranges && limitRangeStore.isLoading && }
+ {this.limitranges.map(limitrange => {
+ return (
+
+ {limitrange.getName()}
+
+ );
+ })}
+
);
}
diff --git a/src/renderer/components/+namespaces/namespace-select-filter.tsx b/src/renderer/components/+namespaces/namespace-select-filter.tsx
new file mode 100644
index 0000000000..85a6299b17
--- /dev/null
+++ b/src/renderer/components/+namespaces/namespace-select-filter.tsx
@@ -0,0 +1,77 @@
+import "./namespace-select.scss";
+
+import React from "react";
+import { observer } from "mobx-react";
+import { components, PlaceholderProps } from "react-select";
+
+import { Icon } from "../icon";
+import { FilterIcon } from "../item-object-list/filter-icon";
+import { FilterType } from "../item-object-list/page-filters.store";
+import { SelectOption } from "../select";
+import { NamespaceSelect } from "./namespace-select";
+import { namespaceStore } from "./namespace.store";
+
+const Placeholder = observer((props: PlaceholderProps) => {
+ const getPlaceholder = (): React.ReactNode => {
+ const namespaces = namespaceStore.contextNamespaces;
+
+ switch (namespaces.length) {
+ case 0:
+ case namespaceStore.allowedNamespaces.length:
+ return <>All namespaces>;
+ case 1:
+ return <>Namespace: {namespaces[0]}>;
+ default:
+ return <>Namespaces: {namespaces.join(", ")}>;
+ }
+ };
+
+ return (
+
+ {getPlaceholder()}
+
+ );
+});
+
+
+@observer
+export class NamespaceSelectFilter extends React.Component {
+ formatOptionLabel({ value: namespace, label }: SelectOption) {
+ if (namespace) {
+ const isSelected = namespaceStore.hasContext(namespace);
+
+ return (
+
+
+ {namespace}
+ {isSelected && }
+
+ );
+ }
+
+ return label;
+ }
+
+ onChange([{ value: namespace }]: SelectOption[]) {
+ if (namespace) {
+ namespaceStore.toggleContext(namespace);
+ } else {
+ namespaceStore.toggleAll(false); // "All namespaces" clicked
+ }
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/src/renderer/components/+namespaces/namespace-select.tsx b/src/renderer/components/+namespaces/namespace-select.tsx
index 079a9cd0b6..775cace4d3 100644
--- a/src/renderer/components/+namespaces/namespace-select.tsx
+++ b/src/renderer/components/+namespaces/namespace-select.tsx
@@ -2,53 +2,50 @@ import "./namespace-select.scss";
import React from "react";
import { computed } from "mobx";
-import { observer } from "mobx-react";
+import { disposeOnUnmount, observer } from "mobx-react";
import { Select, SelectOption, SelectProps } from "../select";
-import { cssNames, noop } from "../../utils";
+import { cssNames } from "../../utils";
import { Icon } from "../icon";
import { namespaceStore } from "./namespace.store";
-import { FilterIcon } from "../item-object-list/filter-icon";
-import { FilterType } from "../item-object-list/page-filters.store";
+import { kubeWatchApi } from "../../api/kube-watch-api";
interface Props extends SelectProps {
showIcons?: boolean;
- showClusterOption?: boolean; // show cluster option on the top (default: false)
- clusterOptionLabel?: React.ReactNode; // label for cluster option (default: "Cluster")
- customizeOptions?(nsOptions: SelectOption[]): SelectOption[];
+ showClusterOption?: boolean; // show "Cluster" option on the top (default: false)
+ showAllNamespacesOption?: boolean; // show "All namespaces" option on the top (default: false)
+ customizeOptions?(options: SelectOption[]): SelectOption[];
}
const defaultProps: Partial = {
showIcons: true,
showClusterOption: false,
- get clusterOptionLabel() {
- return `Cluster`;
- },
};
@observer
export class NamespaceSelect extends React.Component {
static defaultProps = defaultProps as object;
- private unsubscribe = noop;
- async componentDidMount() {
- if (!namespaceStore.isLoaded) {
- await namespaceStore.loadAll();
- }
- this.unsubscribe = namespaceStore.subscribe();
+ componentDidMount() {
+ disposeOnUnmount(this, [
+ kubeWatchApi.subscribeStores([namespaceStore], {
+ preload: true,
+ loadOnce: true, // skip reloading namespaces on every render / page visit
+ })
+ ]);
}
- componentWillUnmount() {
- this.unsubscribe();
- }
-
- @computed get options(): SelectOption[] {
- const { customizeOptions, showClusterOption, clusterOptionLabel } = this.props;
+ @computed.struct get options(): SelectOption[] {
+ const { customizeOptions, showClusterOption, showAllNamespacesOption } = this.props;
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
- options = customizeOptions ? customizeOptions(options) : options;
+ if (showAllNamespacesOption) {
+ options.unshift({ label: "All Namespaces", value: "" });
+ } else if (showClusterOption) {
+ options.unshift({ label: "Cluster", value: "" });
+ }
- if (showClusterOption) {
- options.unshift({ value: null, label: clusterOptionLabel });
+ if (customizeOptions) {
+ options = customizeOptions(options);
}
return options;
@@ -60,14 +57,14 @@ export class NamespaceSelect extends React.Component {
return label || (
<>
- {showIcons && }
+ {showIcons && }
{value}
>
);
};
render() {
- const { className, showIcons, showClusterOption, clusterOptionLabel, customizeOptions, ...selectProps } = this.props;
+ const { className, showIcons, customizeOptions, ...selectProps } = this.props;
return (
{
);
}
}
-
-@observer
-export class NamespaceSelectFilter extends React.Component {
- render() {
- const { contextNs, hasContext, toggleContext } = namespaceStore;
- let placeholder = <>All namespaces>;
-
- if (contextNs.length == 1) placeholder = <>Namespace: {contextNs[0]}>;
- if (contextNs.length >= 2) placeholder = <>Namespaces: {contextNs.join(", ")}>;
-
- return (
- false}
- controlShouldRenderValue={false}
- isMulti
- onChange={([{ value }]: SelectOption[]) => toggleContext(value)}
- formatOptionLabel={({ value: namespace }: SelectOption) => {
- const isSelected = hasContext(namespace);
-
- return (
-
-
- {namespace}
- {isSelected && }
-
- );
- }}
- />
- );
- }
-}
diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts
index ad02dd137c..9995fbb7e5 100644
--- a/src/renderer/components/+namespaces/namespace.store.ts
+++ b/src/renderer/components/+namespaces/namespace.store.ts
@@ -1,120 +1,182 @@
-import { action, comparer, observable, reaction } from "mobx";
+import { action, comparer, computed, IReactionDisposer, IReactionOptions, observable, reaction } from "mobx";
import { autobind, createStorage } from "../../utils";
-import { KubeObjectStore } from "../../kube-object.store";
-import { Namespace, namespacesApi } from "../../api/endpoints";
+import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
+import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
import { createPageParam } from "../../navigation";
import { apiManager } from "../../api/api-manager";
-import { isAllowedResource } from "../../../common/rbac";
-import { getHostedCluster } from "../../../common/cluster-store";
-const storage = createStorage("context_namespaces", []);
+const storage = createStorage("context_namespaces");
export const namespaceUrlParam = createPageParam({
name: "namespaces",
isSystem: true,
multiValues: true,
get defaultValue() {
- return storage.get(); // initial namespaces coming from URL or local-storage (default)
+ return storage.get() ?? []; // initial namespaces coming from URL or local-storage (default)
}
});
+export function getDummyNamespace(name: string) {
+ return new Namespace({
+ kind: Namespace.kind,
+ apiVersion: "v1",
+ metadata: {
+ name,
+ uid: "",
+ resourceVersion: "",
+ selfLink: `/api/v1/namespaces/${name}`
+ }
+ });
+}
+
@autobind()
export class NamespaceStore extends KubeObjectStore {
api = namespacesApi;
- contextNs = observable.array();
+
+ @observable private contextNs = observable.set();
constructor() {
super();
this.init();
}
- private init() {
- this.setContext(this.initNamespaces);
+ private async init() {
+ await this.contextReady;
- return reaction(() => this.contextNs.toJS(), namespaces => {
+ this.setContext(this.initialNamespaces);
+ this.autoLoadAllowedNamespaces();
+ this.autoUpdateUrlAndLocalStorage();
+ }
+
+ public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
+ return reaction(() => Array.from(this.contextNs), callback, {
+ equals: comparer.shallow,
+ ...opts,
+ });
+ }
+
+ private autoUpdateUrlAndLocalStorage(): IReactionDisposer {
+ return this.onContextChange(namespaces => {
storage.set(namespaces); // save to local-storage
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
}, {
fireImmediately: true,
- equals: comparer.identity,
});
}
- get initNamespaces() {
- return namespaceUrlParam.get();
+ private autoLoadAllowedNamespaces(): IReactionDisposer {
+ return reaction(() => this.allowedNamespaces, namespaces => this.loadAll({ namespaces }), {
+ fireImmediately: true,
+ equals: comparer.shallow,
+ });
}
- getContextParams() {
- return {
- namespaces: this.contextNs.toJS(),
- };
- }
+ @computed
+ private get initialNamespaces(): string[] {
+ const namespaces = new Set(this.allowedNamespaces);
+ const prevSelectedNamespaces = storage.get();
- subscribe(apis = [this.api]) {
- const { accessibleNamespaces } = getHostedCluster();
-
- // if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
- if (accessibleNamespaces.length > 0) {
- return Function; // no-op
+ // return previously saved namespaces from local-storage (if any)
+ if (prevSelectedNamespaces) {
+ return prevSelectedNamespaces.filter(namespace => namespaces.has(namespace));
}
- return super.subscribe(apis);
+ // otherwise select "default" or first allowed namespace
+ if (namespaces.has("default")) {
+ return ["default"];
+ } else if (namespaces.size) {
+ return [Array.from(namespaces)[0]];
+ }
+
+ return [];
}
- protected async loadItems(namespaces?: string[]) {
- if (!isAllowedResource("namespaces")) {
- if (namespaces) return namespaces.map(this.getDummyNamespace);
+ @computed get allowedNamespaces(): string[] {
+ return Array.from(new Set([
+ ...(this.context?.allNamespaces ?? []), // allowed namespaces from cluster (main), updating every 30s
+ ...this.items.map(item => item.getName()), // loaded namespaces from k8s api
+ ].flat()));
+ }
+ @computed get contextNamespaces(): string[] {
+ const namespaces = Array.from(this.contextNs);
+
+ if (!namespaces.length) {
+ return this.allowedNamespaces; // show all namespaces when nothing selected
+ }
+
+ return namespaces;
+ }
+
+ getSubscribeApis() {
+ // if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
+ if (this.context?.cluster.accessibleNamespaces.length > 0) {
return [];
}
- if (namespaces) {
- return Promise.all(namespaces.map(name => this.api.get({ name })));
- } else {
- return super.loadItems();
- }
+ return super.getSubscribeApis();
}
- protected getDummyNamespace(name: string) {
- return new Namespace({
- kind: "Namespace",
- apiVersion: "v1",
- metadata: {
- name,
- uid: "",
- resourceVersion: "",
- selfLink: `/api/v1/namespaces/${name}`
- }
- });
+ protected async loadItems(params: KubeObjectStoreLoadingParams) {
+ const { allowedNamespaces } = this;
+
+ let namespaces = (await super.loadItems(params)) || [];
+
+ namespaces = namespaces.filter(namespace => allowedNamespaces.includes(namespace.getName()));
+
+ if (!namespaces.length && allowedNamespaces.length > 0) {
+ return allowedNamespaces.map(getDummyNamespace);
+ }
+
+ return namespaces;
}
@action
- setContext(namespaces: string[]) {
+ setContext(namespace: string | string[]) {
+ const namespaces = [namespace].flat();
+
this.contextNs.replace(namespaces);
}
- hasContext(namespace: string | string[]) {
- const context = Array.isArray(namespace) ? namespace : [namespace];
+ @action
+ resetContext() {
+ this.contextNs.clear();
+ }
- return context.every(namespace => this.contextNs.includes(namespace));
+ hasContext(namespaces: string | string[]) {
+ return [namespaces].flat().every(namespace => this.contextNs.has(namespace));
+ }
+
+ @computed get hasAllContexts(): boolean {
+ return this.contextNs.size === this.allowedNamespaces.length;
}
@action
toggleContext(namespace: string) {
- if (this.hasContext(namespace)) this.contextNs.remove(namespace);
- else this.contextNs.push(namespace);
+ if (this.hasContext(namespace)) {
+ this.contextNs.delete(namespace);
+ } else {
+ this.contextNs.add(namespace);
+ }
}
@action
- reset() {
- super.reset();
- this.contextNs.clear();
+ toggleAll(showAll?: boolean) {
+ if (typeof showAll === "boolean") {
+ if (showAll) {
+ this.setContext(this.allowedNamespaces);
+ } else {
+ this.resetContext(); // empty context considered as "All namespaces"
+ }
+ } else {
+ this.toggleAll(!this.hasAllContexts);
+ }
}
@action
async remove(item: Namespace) {
await super.remove(item);
- this.contextNs.remove(item.getName());
+ this.contextNs.delete(item.getName());
}
}
diff --git a/src/renderer/components/+namespaces/namespaces.tsx b/src/renderer/components/+namespaces/namespaces.tsx
index f097657493..1aeec0fb44 100644
--- a/src/renderer/components/+namespaces/namespaces.tsx
+++ b/src/renderer/components/+namespaces/namespaces.tsx
@@ -11,7 +11,7 @@ import { INamespacesRouteParams } from "./namespaces.route";
import { namespaceStore } from "./namespace.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
labels = "labels",
age = "age",
@@ -27,12 +27,14 @@ export class Namespaces extends React.Component {
ns.getName(),
- [sortBy.labels]: (ns: Namespace) => ns.getLabels(),
- [sortBy.age]: (ns: Namespace) => ns.metadata.creationTimestamp,
- [sortBy.status]: (ns: Namespace) => ns.getStatus(),
+ [columnId.name]: (ns: Namespace) => ns.getName(),
+ [columnId.labels]: (ns: Namespace) => ns.getLabels(),
+ [columnId.age]: (ns: Namespace) => ns.getTimeDiffFromNow(),
+ [columnId.status]: (ns: Namespace) => ns.getStatus(),
}}
searchFilters={[
(item: Namespace) => item.getSearchFields(),
@@ -40,11 +42,11 @@ export class Namespaces extends React.Component {
]}
renderHeaderTitle="Namespaces"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Labels", className: "labels", sortBy: sortBy.labels },
- { title: "Age", className: "age", sortBy: sortBy.age },
- { title: "Status", className: "status", sortBy: sortBy.status },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Labels", className: "labels", sortBy: columnId.labels, id: columnId.labels },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
+ { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
]}
renderTableContents={(item: Namespace) => [
item.getName(),
diff --git a/src/renderer/components/+network-endpoints/endpoints.tsx b/src/renderer/components/+network-endpoints/endpoints.tsx
index 3b859c46f3..ae37fd10ae 100644
--- a/src/renderer/components/+network-endpoints/endpoints.tsx
+++ b/src/renderer/components/+network-endpoints/endpoints.tsx
@@ -9,9 +9,10 @@ import { endpointStore } from "./endpoints.store";
import { KubeObjectListLayout } from "../kube-object";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
+ endpoints = "endpoints",
age = "age",
}
@@ -23,22 +24,24 @@ export class Endpoints extends React.Component {
render() {
return (
endpoint.getName(),
- [sortBy.namespace]: (endpoint: Endpoint) => endpoint.getNs(),
- [sortBy.age]: (endpoint: Endpoint) => endpoint.metadata.creationTimestamp,
+ [columnId.name]: (endpoint: Endpoint) => endpoint.getName(),
+ [columnId.namespace]: (endpoint: Endpoint) => endpoint.getNs(),
+ [columnId.age]: (endpoint: Endpoint) => endpoint.getTimeDiffFromNow(),
}}
searchFilters={[
(endpoint: Endpoint) => endpoint.getSearchFields()
]}
renderHeaderTitle="Endpoints"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Endpoints", className: "endpoints" },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Endpoints", className: "endpoints", id: columnId.endpoints },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(endpoint: Endpoint) => [
endpoint.getName(),
diff --git a/src/renderer/components/+network-ingresses/ingresses.tsx b/src/renderer/components/+network-ingresses/ingresses.tsx
index adb6c84528..d934127bca 100644
--- a/src/renderer/components/+network-ingresses/ingresses.tsx
+++ b/src/renderer/components/+network-ingresses/ingresses.tsx
@@ -9,9 +9,11 @@ import { ingressStore } from "./ingress.store";
import { KubeObjectListLayout } from "../kube-object";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
+ loadBalancers ="load-balancers",
+ rules = "rules",
age = "age",
}
@@ -23,11 +25,13 @@ export class Ingresses extends React.Component {
render() {
return (
ingress.getName(),
- [sortBy.namespace]: (ingress: Ingress) => ingress.getNs(),
- [sortBy.age]: (ingress: Ingress) => ingress.metadata.creationTimestamp,
+ [columnId.name]: (ingress: Ingress) => ingress.getName(),
+ [columnId.namespace]: (ingress: Ingress) => ingress.getNs(),
+ [columnId.age]: (ingress: Ingress) => ingress.getTimeDiffFromNow(),
}}
searchFilters={[
(ingress: Ingress) => ingress.getSearchFields(),
@@ -35,12 +39,12 @@ export class Ingresses extends React.Component {
]}
renderHeaderTitle="Ingresses"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "LoadBalancers", className: "loadbalancers" },
- { title: "Rules", className: "rules" },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "LoadBalancers", className: "loadbalancers", id: columnId.loadBalancers },
+ { title: "Rules", className: "rules", id: columnId.rules },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(ingress: Ingress) => [
ingress.getName(),
diff --git a/src/renderer/components/+network-policies/network-policies.tsx b/src/renderer/components/+network-policies/network-policies.tsx
index d4dc0e2fa9..62b298edcd 100644
--- a/src/renderer/components/+network-policies/network-policies.tsx
+++ b/src/renderer/components/+network-policies/network-policies.tsx
@@ -9,9 +9,10 @@ import { INetworkPoliciesRouteParams } from "./network-policies.route";
import { networkPolicyStore } from "./network-policy.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
+ types = "types",
age = "age",
}
@@ -23,22 +24,24 @@ export class NetworkPolicies extends React.Component {
render() {
return (
item.getName(),
- [sortBy.namespace]: (item: NetworkPolicy) => item.getNs(),
- [sortBy.age]: (item: NetworkPolicy) => item.metadata.creationTimestamp,
+ [columnId.name]: (item: NetworkPolicy) => item.getName(),
+ [columnId.namespace]: (item: NetworkPolicy) => item.getNs(),
+ [columnId.age]: (item: NetworkPolicy) => item.getTimeDiffFromNow(),
}}
searchFilters={[
(item: NetworkPolicy) => item.getSearchFields(),
]}
renderHeaderTitle="Network Policies"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Policy Types", className: "type" },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Policy Types", className: "type", id: columnId.types },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(item: NetworkPolicy) => [
item.getName(),
diff --git a/src/renderer/components/+network-policies/network-policy-details.tsx b/src/renderer/components/+network-policies/network-policy-details.tsx
index 5e3c39bcf4..4c646e73a8 100644
--- a/src/renderer/components/+network-policies/network-policy-details.tsx
+++ b/src/renderer/components/+network-policies/network-policy-details.tsx
@@ -117,7 +117,7 @@ export class NetworkPolicyDetails extends React.Component {
{ingress && (
<>
-
+
{ingress.map((ingress, i) => {
const { ports } = ingress;
diff --git a/src/renderer/components/+network-services/service-details.tsx b/src/renderer/components/+network-services/service-details.tsx
index 58cbe0a86e..e05df55815 100644
--- a/src/renderer/components/+network-services/service-details.tsx
+++ b/src/renderer/components/+network-services/service-details.tsx
@@ -1,17 +1,18 @@
import "./service-details.scss";
import React from "react";
-import { observer } from "mobx-react";
+import { disposeOnUnmount, observer } from "mobx-react";
import { DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge";
import { KubeEventDetails } from "../+events/kube-event-details";
import { KubeObjectDetailsProps } from "../kube-object";
-import { Service, endpointApi } from "../../api/endpoints";
+import { Service } from "../../api/endpoints";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { ServicePortComponent } from "./service-port-component";
import { endpointStore } from "../+network-endpoints/endpoints.store";
import { ServiceDetailsEndpoint } from "./service-details-endpoint";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
+import { kubeWatchApi } from "../../api/kube-watch-api";
interface Props extends KubeObjectDetailsProps {
}
@@ -19,10 +20,11 @@ interface Props extends KubeObjectDetailsProps {
@observer
export class ServiceDetails extends React.Component {
componentDidMount() {
- if (!endpointStore.isLoaded) {
- endpointStore.loadAll();
- }
- endpointApi.watch();
+ disposeOnUnmount(this, [
+ kubeWatchApi.subscribeStores([endpointStore], {
+ preload: true,
+ }),
+ ]);
}
render() {
@@ -48,7 +50,7 @@ export class ServiceDetails extends React.Component {
{spec.sessionAffinity}
-
+
{spec.clusterIP}
@@ -75,9 +77,9 @@ export class ServiceDetails extends React.Component {
{spec.loadBalancerIP}
)}
-
+
-
+
);
}
diff --git a/src/renderer/components/+network-services/service-port-component.tsx b/src/renderer/components/+network-services/service-port-component.tsx
index 6946137bde..77070390e0 100644
--- a/src/renderer/components/+network-services/service-port-component.tsx
+++ b/src/renderer/components/+network-services/service-port-component.tsx
@@ -37,7 +37,7 @@ export class ServicePortComponent extends React.Component
{
return (
-
this.portForward() }>
+ this.portForward() }>
{port.toString()}
{this.waiting && (
diff --git a/src/renderer/components/+network-services/services.tsx b/src/renderer/components/+network-services/services.tsx
index 3452c10a68..c250f7736c 100644
--- a/src/renderer/components/+network-services/services.tsx
+++ b/src/renderer/components/+network-services/services.tsx
@@ -10,12 +10,13 @@ import { Badge } from "../badge";
import { serviceStore } from "./services.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
selector = "selector",
ports = "port",
clusterIp = "cluster-ip",
+ externalIp = "external-ip",
age = "age",
type = "type",
status = "status",
@@ -29,16 +30,18 @@ export class Services extends React.Component {
render() {
return (
service.getName(),
- [sortBy.namespace]: (service: Service) => service.getNs(),
- [sortBy.selector]: (service: Service) => service.getSelector(),
- [sortBy.ports]: (service: Service) => (service.spec.ports || []).map(({ port }) => port)[0],
- [sortBy.clusterIp]: (service: Service) => service.getClusterIp(),
- [sortBy.type]: (service: Service) => service.getType(),
- [sortBy.age]: (service: Service) => service.metadata.creationTimestamp,
- [sortBy.status]: (service: Service) => service.getStatus(),
+ [columnId.name]: (service: Service) => service.getName(),
+ [columnId.namespace]: (service: Service) => service.getNs(),
+ [columnId.selector]: (service: Service) => service.getSelector(),
+ [columnId.ports]: (service: Service) => (service.spec.ports || []).map(({ port }) => port)[0],
+ [columnId.clusterIp]: (service: Service) => service.getClusterIp(),
+ [columnId.type]: (service: Service) => service.getType(),
+ [columnId.age]: (service: Service) => service.getTimeDiffFromNow(),
+ [columnId.status]: (service: Service) => service.getStatus(),
}}
searchFilters={[
(service: Service) => service.getSearchFields(),
@@ -47,16 +50,16 @@ export class Services extends React.Component {
]}
renderHeaderTitle="Services"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Type", className: "type", sortBy: sortBy.type },
- { title: "Cluster IP", className: "clusterIp", sortBy: sortBy.clusterIp, },
- { title: "Ports", className: "ports", sortBy: sortBy.ports },
- { title: "External IP", className: "externalIp" },
- { title: "Selector", className: "selector", sortBy: sortBy.selector },
- { title: "Age", className: "age", sortBy: sortBy.age },
- { title: "Status", className: "status", sortBy: sortBy.status },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Type", className: "type", sortBy: columnId.type, id: columnId.type },
+ { title: "Cluster IP", className: "clusterIp", sortBy: columnId.clusterIp, id: columnId.clusterIp },
+ { title: "Ports", className: "ports", sortBy: columnId.ports, id: columnId.ports },
+ { title: "External IP", className: "externalIp", id: columnId.externalIp },
+ { title: "Selector", className: "selector", sortBy: columnId.selector, id: columnId.selector },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
+ { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
]}
renderTableContents={(service: Service) => [
service.getName(),
diff --git a/src/renderer/components/+network/index.ts b/src/renderer/components/+network/index.ts
index 9a58dc59a6..e37b11c3b7 100644
--- a/src/renderer/components/+network/index.ts
+++ b/src/renderer/components/+network/index.ts
@@ -1,2 +1,3 @@
export * from "./network.route";
export * from "./network";
+export * from "./network.command";
diff --git a/src/renderer/components/+network/network.command.ts b/src/renderer/components/+network/network.command.ts
new file mode 100644
index 0000000000..6dfa907136
--- /dev/null
+++ b/src/renderer/components/+network/network.command.ts
@@ -0,0 +1,34 @@
+import { navigate } from "../../navigation";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { servicesURL } from "../+network-services";
+import { endpointURL } from "../+network-endpoints";
+import { ingressURL } from "../+network-ingresses";
+import { networkPoliciesURL } from "../+network-policies";
+
+commandRegistry.add({
+ id: "cluster.viewServices",
+ title: "Cluster: View Services",
+ scope: "cluster",
+ action: () => navigate(servicesURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewEndpoints",
+ title: "Cluster: View Endpoints",
+ scope: "cluster",
+ action: () => navigate(endpointURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewIngresses",
+ title: "Cluster: View Ingresses",
+ scope: "cluster",
+ action: () => navigate(ingressURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewNetworkPolicies",
+ title: "Cluster: View NetworkPolicies",
+ scope: "cluster",
+ action: () => navigate(networkPoliciesURL())
+});
diff --git a/src/renderer/components/+nodes/index.ts b/src/renderer/components/+nodes/index.ts
index 515a3d85f6..8af90267d4 100644
--- a/src/renderer/components/+nodes/index.ts
+++ b/src/renderer/components/+nodes/index.ts
@@ -1,3 +1,4 @@
export * from "./nodes";
export * from "./nodes.route";
export * from "./node-details";
+export * from "./node.command";
diff --git a/src/renderer/components/+nodes/node-details.tsx b/src/renderer/components/+nodes/node-details.tsx
index 824affafee..810837d59d 100644
--- a/src/renderer/components/+nodes/node-details.tsx
+++ b/src/renderer/components/+nodes/node-details.tsx
@@ -29,9 +29,7 @@ export class NodeDetails extends React.Component {
});
async componentDidMount() {
- if (!podsStore.isLoaded) {
- podsStore.loadAll();
- }
+ podsStore.reloadAll();
}
componentWillUnmount() {
diff --git a/src/renderer/components/+nodes/node.command.ts b/src/renderer/components/+nodes/node.command.ts
new file mode 100644
index 0000000000..9257cea668
--- /dev/null
+++ b/src/renderer/components/+nodes/node.command.ts
@@ -0,0 +1,10 @@
+import { navigate } from "../../navigation";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { nodesURL } from "./nodes.route";
+
+commandRegistry.add({
+ id: "cluster.viewNodes",
+ title: "Cluster: View Nodes",
+ scope: "cluster",
+ action: () => navigate(nodesURL())
+});
diff --git a/src/renderer/components/+nodes/nodes.store.ts b/src/renderer/components/+nodes/nodes.store.ts
index c0385b078b..b301015747 100644
--- a/src/renderer/components/+nodes/nodes.store.ts
+++ b/src/renderer/components/+nodes/nodes.store.ts
@@ -1,3 +1,4 @@
+import { sum } from "lodash";
import { action, computed, observable } from "mobx";
import { clusterApi, IClusterMetrics, INodeMetrics, Node, nodesApi } from "../../api/endpoints";
import { autobind } from "../../utils";
@@ -62,6 +63,10 @@ export class NodesStore extends KubeObjectStore {
});
}
+ getWarningsCount(): number {
+ return sum(this.items.map((node: Node) => node.getWarningConditions().length));
+ }
+
reset() {
super.reset();
this.metrics = {};
diff --git a/src/renderer/components/+nodes/nodes.tsx b/src/renderer/components/+nodes/nodes.tsx
index edfa5c4026..6237bf4056 100644
--- a/src/renderer/components/+nodes/nodes.tsx
+++ b/src/renderer/components/+nodes/nodes.tsx
@@ -17,7 +17,7 @@ import upperFirst from "lodash/upperFirst";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { Badge } from "../badge/badge";
-enum sortBy {
+enum columnId {
name = "name",
cpu = "cpu",
memory = "memory",
@@ -51,6 +51,10 @@ export class Nodes extends React.Component {
if (!metrics || !metrics[1]) return ;
const usage = metrics[0];
const cores = metrics[1];
+ const cpuUsagePercent = Math.ceil(usage * 100) / cores;
+ const cpuUsagePercentLabel: String = cpuUsagePercent % 1 === 0
+ ? cpuUsagePercent.toString()
+ : cpuUsagePercent.toFixed(2);
return (
{
value={usage}
tooltip={{
preferredPositions: TooltipPosition.BOTTOM,
- children: `CPU: ${Math.ceil(usage * 100) / cores}\%, cores: ${cores}`
+ children: `CPU: ${cpuUsagePercentLabel}\%, cores: ${cores}`
}}
/>
);
@@ -131,21 +135,23 @@ export class Nodes extends React.Component {
return (
node.getName(),
- [sortBy.cpu]: (node: Node) => nodesStore.getLastMetricValues(node, ["cpuUsage"]),
- [sortBy.memory]: (node: Node) => nodesStore.getLastMetricValues(node, ["memoryUsage"]),
- [sortBy.disk]: (node: Node) => nodesStore.getLastMetricValues(node, ["fsUsage"]),
- [sortBy.conditions]: (node: Node) => node.getNodeConditionText(),
- [sortBy.taints]: (node: Node) => node.getTaints().length,
- [sortBy.roles]: (node: Node) => node.getRoleLabels(),
- [sortBy.age]: (node: Node) => node.metadata.creationTimestamp,
- [sortBy.version]: (node: Node) => node.getKubeletVersion(),
+ [columnId.name]: (node: Node) => node.getName(),
+ [columnId.cpu]: (node: Node) => nodesStore.getLastMetricValues(node, ["cpuUsage"]),
+ [columnId.memory]: (node: Node) => nodesStore.getLastMetricValues(node, ["memoryUsage"]),
+ [columnId.disk]: (node: Node) => nodesStore.getLastMetricValues(node, ["fsUsage"]),
+ [columnId.conditions]: (node: Node) => node.getNodeConditionText(),
+ [columnId.taints]: (node: Node) => node.getTaints().length,
+ [columnId.roles]: (node: Node) => node.getRoleLabels(),
+ [columnId.age]: (node: Node) => node.getTimeDiffFromNow(),
+ [columnId.version]: (node: Node) => node.getKubeletVersion(),
}}
searchFilters={[
(node: Node) => node.getSearchFields(),
@@ -155,16 +161,16 @@ export class Nodes extends React.Component {
]}
renderHeaderTitle="Nodes"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "CPU", className: "cpu", sortBy: sortBy.cpu },
- { title: "Memory", className: "memory", sortBy: sortBy.memory },
- { title: "Disk", className: "disk", sortBy: sortBy.disk },
- { title: "Taints", className: "taints", sortBy: sortBy.taints },
- { title: "Roles", className: "roles", sortBy: sortBy.roles },
- { title: "Version", className: "version", sortBy: sortBy.version },
- { title: "Age", className: "age", sortBy: sortBy.age },
- { title: "Conditions", className: "conditions", sortBy: sortBy.conditions },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "CPU", className: "cpu", sortBy: columnId.cpu, id: columnId.cpu },
+ { title: "Memory", className: "memory", sortBy: columnId.memory, id: columnId.memory },
+ { title: "Disk", className: "disk", sortBy: columnId.disk, id: columnId.disk },
+ { title: "Taints", className: "taints", sortBy: columnId.taints, id: columnId.taints },
+ { title: "Roles", className: "roles", sortBy: columnId.roles, id: columnId.roles },
+ { title: "Version", className: "version", sortBy: columnId.version, id: columnId.version },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
+ { title: "Conditions", className: "conditions", sortBy: columnId.conditions, id: columnId.conditions },
]}
renderTableContents={(node: Node) => {
const tooltipId = `node-taints-${node.getId()}`;
diff --git a/src/renderer/components/+pod-security-policies/index.ts b/src/renderer/components/+pod-security-policies/index.ts
index d037873b5b..c9379d3381 100644
--- a/src/renderer/components/+pod-security-policies/index.ts
+++ b/src/renderer/components/+pod-security-policies/index.ts
@@ -1,3 +1,3 @@
export * from "./pod-security-policies.route";
export * from "./pod-security-policies";
-export * from "./pod-security-policy-details";
\ No newline at end of file
+export * from "./pod-security-policy-details";
diff --git a/src/renderer/components/+pod-security-policies/pod-security-policies.tsx b/src/renderer/components/+pod-security-policies/pod-security-policies.tsx
index 30ec1d6304..affd9f13eb 100644
--- a/src/renderer/components/+pod-security-policies/pod-security-policies.tsx
+++ b/src/renderer/components/+pod-security-policies/pod-security-policies.tsx
@@ -7,7 +7,7 @@ import { podSecurityPoliciesStore } from "./pod-security-policies.store";
import { PodSecurityPolicy } from "../../api/endpoints";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
volumes = "volumes",
privileged = "privileged",
@@ -19,14 +19,16 @@ export class PodSecurityPolicies extends React.Component {
render() {
return (
item.getName(),
- [sortBy.volumes]: (item: PodSecurityPolicy) => item.getVolumes(),
- [sortBy.privileged]: (item: PodSecurityPolicy) => +item.isPrivileged(),
- [sortBy.age]: (item: PodSecurityPolicy) => item.metadata.creationTimestamp,
+ [columnId.name]: (item: PodSecurityPolicy) => item.getName(),
+ [columnId.volumes]: (item: PodSecurityPolicy) => item.getVolumes(),
+ [columnId.privileged]: (item: PodSecurityPolicy) => +item.isPrivileged(),
+ [columnId.age]: (item: PodSecurityPolicy) => item.getTimeDiffFromNow(),
}}
searchFilters={[
(item: PodSecurityPolicy) => item.getSearchFields(),
@@ -35,11 +37,11 @@ export class PodSecurityPolicies extends React.Component {
]}
renderHeaderTitle="Pod Security Policies"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Privileged", className: "privileged", sortBy: sortBy.privileged },
- { title: "Volumes", className: "volumes", sortBy: sortBy.volumes },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Privileged", className: "privileged", sortBy: columnId.privileged, id: columnId.privileged },
+ { title: "Volumes", className: "volumes", sortBy: columnId.volumes, id: columnId.volumes },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(item: PodSecurityPolicy) => {
return [
diff --git a/src/renderer/components/+preferences/add-helm-repo-dialog.tsx b/src/renderer/components/+preferences/add-helm-repo-dialog.tsx
index f2c8388247..a0ede38ece 100644
--- a/src/renderer/components/+preferences/add-helm-repo-dialog.tsx
+++ b/src/renderer/components/+preferences/add-helm-repo-dialog.tsx
@@ -111,7 +111,7 @@ export class AddHelmRepoDialog extends React.Component {
<>
this.helmRepo.insecureSkipTlsVerify = v}
/>
@@ -120,12 +120,12 @@ export class AddHelmRepoDialog extends React.Component {
{this.renderFileInput(`Cerificate file`, FileType.CertFile, AddHelmRepoDialog.certExtensions)}
this.helmRepo.username = v}
/>
this.helmRepo.password = v}
/>
>);
@@ -148,13 +148,13 @@ export class AddHelmRepoDialog extends React.Component {
this.helmRepo.name = v}
/>
this.helmRepo.url = v}
/>
@@ -162,7 +162,7 @@ export class AddHelmRepoDialog extends React.Component
{
More
diff --git a/src/renderer/components/+preferences/preferences.route.ts b/src/renderer/components/+preferences/preferences.route.ts
index 7c880bd5fc..2915b68a28 100644
--- a/src/renderer/components/+preferences/preferences.route.ts
+++ b/src/renderer/components/+preferences/preferences.route.ts
@@ -1,8 +1,17 @@
import type { RouteProps } from "react-router";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
import { buildURL } from "../../../common/utils/buildUrl";
+import { navigate } from "../../navigation";
export const preferencesRoute: RouteProps = {
path: "/preferences"
};
export const preferencesURL = buildURL(preferencesRoute.path);
+
+commandRegistry.add({
+ id: "app.showPreferences",
+ title: "Preferences: Open",
+ scope: "global",
+ action: () => navigate(preferencesURL())
+});
diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx
index 56e881d9f5..b0442f45b6 100644
--- a/src/renderer/components/+preferences/preferences.tsx
+++ b/src/renderer/components/+preferences/preferences.tsx
@@ -122,7 +122,7 @@ export class Preferences extends React.Component {
HTTP Proxy
this.httpProxy = v}
onBlur={() => preferences.httpsProxy = this.httpProxy}
diff --git a/src/renderer/components/+storage-classes/storage-class-details.tsx b/src/renderer/components/+storage-classes/storage-class-details.tsx
index 95f09bc6bf..1d47df8ecb 100644
--- a/src/renderer/components/+storage-classes/storage-class-details.tsx
+++ b/src/renderer/components/+storage-classes/storage-class-details.tsx
@@ -10,14 +10,22 @@ import { KubeObjectDetailsProps } from "../kube-object";
import { StorageClass } from "../../api/endpoints";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
+import { storageClassStore } from "./storage-class.store";
+import { VolumeDetailsList } from "../+storage-volumes/volume-details-list";
+import { volumesStore } from "../+storage-volumes/volumes.store";
interface Props extends KubeObjectDetailsProps {
}
@observer
export class StorageClassDetails extends React.Component {
+ async componentDidMount() {
+ volumesStore.reloadAll();
+ }
+
render() {
const { object: storageClass } = this.props;
+ const persistentVolumes = storageClassStore.getPersistentVolumes(storageClass);
if (!storageClass) return null;
const { provisioner, parameters, mountOptions } = storageClass;
@@ -45,7 +53,7 @@ export class StorageClassDetails extends React.Component {
)}
{parameters && (
<>
-
+
{
Object.entries(parameters).map(([name, value]) => (
@@ -55,6 +63,7 @@ export class StorageClassDetails extends React.Component {
}
>
)}
+
);
}
diff --git a/src/renderer/components/+storage-classes/storage-class.store.ts b/src/renderer/components/+storage-classes/storage-class.store.ts
index 9e388456a5..0050c432b5 100644
--- a/src/renderer/components/+storage-classes/storage-class.store.ts
+++ b/src/renderer/components/+storage-classes/storage-class.store.ts
@@ -2,10 +2,15 @@ import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { StorageClass, storageClassApi } from "../../api/endpoints/storage-class.api";
import { apiManager } from "../../api/api-manager";
+import { volumesStore } from "../+storage-volumes/volumes.store";
@autobind()
export class StorageClassStore extends KubeObjectStore {
api = storageClassApi;
+
+ getPersistentVolumes(storageClass: StorageClass) {
+ return volumesStore.getByStorageClass(storageClass);
+ }
}
export const storageClassStore = new StorageClassStore();
diff --git a/src/renderer/components/+storage-classes/storage-classes.tsx b/src/renderer/components/+storage-classes/storage-classes.tsx
index ec7e1c8e05..f4f0432afd 100644
--- a/src/renderer/components/+storage-classes/storage-classes.tsx
+++ b/src/renderer/components/+storage-classes/storage-classes.tsx
@@ -9,10 +9,11 @@ import { IStorageClassesRouteParams } from "./storage-classes.route";
import { storageClassStore } from "./storage-class.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
age = "age",
provisioner = "provision",
+ default = "default",
reclaimPolicy = "reclaim",
}
@@ -24,13 +25,15 @@ export class StorageClasses extends React.Component {
render() {
return (
item.getName(),
- [sortBy.age]: (item: StorageClass) => item.metadata.creationTimestamp,
- [sortBy.provisioner]: (item: StorageClass) => item.provisioner,
- [sortBy.reclaimPolicy]: (item: StorageClass) => item.reclaimPolicy,
+ [columnId.name]: (item: StorageClass) => item.getName(),
+ [columnId.age]: (item: StorageClass) => item.getTimeDiffFromNow(),
+ [columnId.provisioner]: (item: StorageClass) => item.provisioner,
+ [columnId.reclaimPolicy]: (item: StorageClass) => item.reclaimPolicy,
}}
searchFilters={[
(item: StorageClass) => item.getSearchFields(),
@@ -38,12 +41,12 @@ export class StorageClasses extends React.Component {
]}
renderHeaderTitle="Storage Classes"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Provisioner", className: "provisioner", sortBy: sortBy.provisioner },
- { title: "Reclaim Policy", className: "reclaim-policy", sortBy: sortBy.reclaimPolicy },
- { title: "Default", className: "is-default" },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Provisioner", className: "provisioner", sortBy: columnId.provisioner, id: columnId.provisioner },
+ { title: "Reclaim Policy", className: "reclaim-policy", sortBy: columnId.reclaimPolicy, id: columnId.reclaimPolicy },
+ { title: "Default", className: "is-default", id: columnId.default },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(storageClass: StorageClass) => [
storageClass.getName(),
diff --git a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx
index ea1ec71778..cbf8a9d4fc 100644
--- a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx
+++ b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx
@@ -71,7 +71,7 @@ export class PersistentVolumeClaimDetails extends React.Component {
{volumeClaim.getStatus()}
-
+
{volumeClaim.getMatchLabels().map(label => )}
diff --git a/src/renderer/components/+storage-volume-claims/volume-claims.tsx b/src/renderer/components/+storage-volume-claims/volume-claims.tsx
index bb9a4a05a7..8a69f6ee60 100644
--- a/src/renderer/components/+storage-volume-claims/volume-claims.tsx
+++ b/src/renderer/components/+storage-volume-claims/volume-claims.tsx
@@ -13,7 +13,7 @@ import { stopPropagation } from "../../utils";
import { storageClassApi } from "../../api/endpoints";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
pods = "pods",
@@ -31,17 +31,19 @@ export class PersistentVolumeClaims extends React.Component {
render() {
return (
pvc.getName(),
- [sortBy.namespace]: (pvc: PersistentVolumeClaim) => pvc.getNs(),
- [sortBy.pods]: (pvc: PersistentVolumeClaim) => pvc.getPods(podsStore.items).map(pod => pod.getName()),
- [sortBy.status]: (pvc: PersistentVolumeClaim) => pvc.getStatus(),
- [sortBy.size]: (pvc: PersistentVolumeClaim) => unitsToBytes(pvc.getStorage()),
- [sortBy.storageClass]: (pvc: PersistentVolumeClaim) => pvc.spec.storageClassName,
- [sortBy.age]: (pvc: PersistentVolumeClaim) => pvc.metadata.creationTimestamp,
+ [columnId.name]: (pvc: PersistentVolumeClaim) => pvc.getName(),
+ [columnId.namespace]: (pvc: PersistentVolumeClaim) => pvc.getNs(),
+ [columnId.pods]: (pvc: PersistentVolumeClaim) => pvc.getPods(podsStore.items).map(pod => pod.getName()),
+ [columnId.status]: (pvc: PersistentVolumeClaim) => pvc.getStatus(),
+ [columnId.size]: (pvc: PersistentVolumeClaim) => unitsToBytes(pvc.getStorage()),
+ [columnId.storageClass]: (pvc: PersistentVolumeClaim) => pvc.spec.storageClassName,
+ [columnId.age]: (pvc: PersistentVolumeClaim) => pvc.getTimeDiffFromNow(),
}}
searchFilters={[
(item: PersistentVolumeClaim) => item.getSearchFields(),
@@ -49,14 +51,14 @@ export class PersistentVolumeClaims extends React.Component {
]}
renderHeaderTitle="Persistent Volume Claims"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Storage class", className: "storageClass", sortBy: sortBy.storageClass },
- { title: "Size", className: "size", sortBy: sortBy.size },
- { title: "Pods", className: "pods", sortBy: sortBy.pods },
- { title: "Age", className: "age", sortBy: sortBy.age },
- { title: "Status", className: "status", sortBy: sortBy.status },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Storage class", className: "storageClass", sortBy: columnId.storageClass, id: columnId.storageClass },
+ { title: "Size", className: "size", sortBy: columnId.size, id: columnId.size },
+ { title: "Pods", className: "pods", sortBy: columnId.pods, id: columnId.pods },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
+ { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
]}
renderTableContents={(pvc: PersistentVolumeClaim) => {
const pods = pvc.getPods(podsStore.items);
diff --git a/src/renderer/components/+storage-volumes/volume-details-list.scss b/src/renderer/components/+storage-volumes/volume-details-list.scss
new file mode 100644
index 0000000000..aed61b4b5f
--- /dev/null
+++ b/src/renderer/components/+storage-volumes/volume-details-list.scss
@@ -0,0 +1,31 @@
+@import "../+storage/storage-mixins";
+
+.VolumeDetailsList {
+ position: relative;
+
+ .Table {
+ margin: 0 (-$margin * 3);
+
+ &.virtual {
+ height: 500px; // applicable for 100+ items
+ }
+ }
+
+ .TableCell {
+ &:first-child {
+ margin-left: $margin;
+ }
+
+ &:last-child {
+ margin-right: $margin;
+ }
+
+ &.name {
+ flex-grow: 2;
+ }
+
+ &.status {
+ @include pv-status-colors;
+ }
+ }
+}
diff --git a/src/renderer/components/+storage-volumes/volume-details-list.tsx b/src/renderer/components/+storage-volumes/volume-details-list.tsx
new file mode 100644
index 0000000000..91dbcf3dd1
--- /dev/null
+++ b/src/renderer/components/+storage-volumes/volume-details-list.tsx
@@ -0,0 +1,88 @@
+import "./volume-details-list.scss";
+
+import React from "react";
+import { observer } from "mobx-react";
+import { PersistentVolume } from "../../api/endpoints/persistent-volume.api";
+import { autobind } from "../../../common/utils/autobind";
+import { TableRow } from "../table/table-row";
+import { cssNames, prevDefault } from "../../utils";
+import { showDetails } from "../kube-object/kube-object-details";
+import { TableCell } from "../table/table-cell";
+import { Spinner } from "../spinner/spinner";
+import { DrawerTitle } from "../drawer/drawer-title";
+import { Table } from "../table/table";
+import { TableHead } from "../table/table-head";
+import { volumesStore } from "./volumes.store";
+import kebabCase from "lodash/kebabCase";
+
+interface Props {
+ persistentVolumes: PersistentVolume[];
+}
+
+enum sortBy {
+ name = "name",
+ status = "status",
+ capacity = "capacity",
+}
+
+@observer
+export class VolumeDetailsList extends React.Component {
+ private sortingCallbacks = {
+ [sortBy.name]: (volume: PersistentVolume) => volume.getName(),
+ [sortBy.capacity]: (volume: PersistentVolume) => volume.getCapacity(),
+ [sortBy.status]: (volume: PersistentVolume) => volume.getStatus(),
+ };
+
+ @autobind()
+ getTableRow(uid: string) {
+ const { persistentVolumes } = this.props;
+ const volume = persistentVolumes.find(volume => volume.getId() === uid);
+
+ return (
+ showDetails(volume.selfLink, false))}
+ >
+ {volume.getName()}
+ {volume.getCapacity()}
+ {volume.getStatus()}
+
+ );
+ }
+
+ render() {
+ const { persistentVolumes } = this.props;
+ const virtual = persistentVolumes.length > 100;
+
+ if (!persistentVolumes.length) {
+ return !volumesStore.isLoaded && ;
+ }
+
+ return (
+
+
+
+
+ Name
+ Capacity
+ Status
+
+ {
+ !virtual && persistentVolumes.map(volume => this.getTableRow(volume.getId()))
+ }
+
+
+ );
+ }
+}
diff --git a/src/renderer/components/+storage-volumes/volumes.scss b/src/renderer/components/+storage-volumes/volumes.scss
index 272aa3fd89..9aa1e616b1 100644
--- a/src/renderer/components/+storage-volumes/volumes.scss
+++ b/src/renderer/components/+storage-volumes/volumes.scss
@@ -26,7 +26,7 @@
flex-grow: 3;
}
- .status {
+ &.status {
@include pv-status-colors;
}
diff --git a/src/renderer/components/+storage-volumes/volumes.store.ts b/src/renderer/components/+storage-volumes/volumes.store.ts
index ea61525735..b2601f9b12 100644
--- a/src/renderer/components/+storage-volumes/volumes.store.ts
+++ b/src/renderer/components/+storage-volumes/volumes.store.ts
@@ -2,10 +2,17 @@ import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { PersistentVolume, persistentVolumeApi } from "../../api/endpoints/persistent-volume.api";
import { apiManager } from "../../api/api-manager";
+import { StorageClass } from "../../api/endpoints/storage-class.api";
@autobind()
export class PersistentVolumesStore extends KubeObjectStore {
api = persistentVolumeApi;
+
+ getByStorageClass(storageClass: StorageClass): PersistentVolume[] {
+ return this.items.filter(volume =>
+ volume.getStorageClassName() === storageClass.getName()
+ );
+ }
}
export const volumesStore = new PersistentVolumesStore();
diff --git a/src/renderer/components/+storage-volumes/volumes.tsx b/src/renderer/components/+storage-volumes/volumes.tsx
index 412093a7ad..55a9eb753e 100644
--- a/src/renderer/components/+storage-volumes/volumes.tsx
+++ b/src/renderer/components/+storage-volumes/volumes.tsx
@@ -11,10 +11,11 @@ import { volumesStore } from "./volumes.store";
import { pvcApi, storageClassApi } from "../../api/endpoints";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
storageClass = "storage-class",
capacity = "capacity",
+ claim = "claim",
status = "status",
age = "age",
}
@@ -27,14 +28,16 @@ export class PersistentVolumes extends React.Component {
render() {
return (
item.getName(),
- [sortBy.storageClass]: (item: PersistentVolume) => item.spec.storageClassName,
- [sortBy.capacity]: (item: PersistentVolume) => item.getCapacity(true),
- [sortBy.status]: (item: PersistentVolume) => item.getStatus(),
- [sortBy.age]: (item: PersistentVolume) => item.metadata.creationTimestamp,
+ [columnId.name]: (item: PersistentVolume) => item.getName(),
+ [columnId.storageClass]: (item: PersistentVolume) => item.getStorageClass(),
+ [columnId.capacity]: (item: PersistentVolume) => item.getCapacity(true),
+ [columnId.status]: (item: PersistentVolume) => item.getStatus(),
+ [columnId.age]: (item: PersistentVolume) => item.getTimeDiffFromNow(),
}}
searchFilters={[
(item: PersistentVolume) => item.getSearchFields(),
@@ -42,13 +45,13 @@ export class PersistentVolumes extends React.Component {
]}
renderHeaderTitle="Persistent Volumes"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Storage Class", className: "storageClass", sortBy: sortBy.storageClass },
- { title: "Capacity", className: "capacity", sortBy: sortBy.capacity },
- { title: "Claim", className: "claim" },
- { title: "Age", className: "age", sortBy: sortBy.age },
- { title: "Status", className: "status", sortBy: sortBy.status },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Storage Class", className: "storageClass", sortBy: columnId.storageClass, id: columnId.storageClass },
+ { title: "Capacity", className: "capacity", sortBy: columnId.capacity, id: columnId.capacity },
+ { title: "Claim", className: "claim", id: columnId.claim },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
+ { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
]}
renderTableContents={(volume: PersistentVolume) => {
const { claimRef, storageClassName } = volume.spec;
diff --git a/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx b/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx
index ab06126bd2..e7a676365c 100644
--- a/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx
+++ b/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx
@@ -7,7 +7,7 @@ import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { Select, SelectOption } from "../select";
import { SubTitle } from "../layout/sub-title";
-import { IRoleBindingSubject, RoleBinding, ServiceAccount, Role } from "../../api/endpoints";
+import { IRoleBindingSubject, Role, RoleBinding, ServiceAccount } from "../../api/endpoints";
import { Icon } from "../icon";
import { Input } from "../input";
import { NamespaceSelect } from "../+namespaces/namespace-select";
@@ -19,6 +19,7 @@ import { namespaceStore } from "../+namespaces/namespace.store";
import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store";
import { roleBindingsStore } from "./role-bindings.store";
import { showDetails } from "../kube-object";
+import { KubeObjectStore } from "../../kube-object.store";
interface BindingSelectOption extends SelectOption {
value: string; // binding name
@@ -73,14 +74,14 @@ export class AddRoleBindingDialog extends React.Component {
};
async loadData() {
- const stores = [
+ const stores: KubeObjectStore[] = [
namespaceStore,
rolesStore,
serviceAccountsStore,
];
this.isLoading = true;
- await Promise.all(stores.map(store => store.loadAll()));
+ await Promise.all(stores.map(store => store.reloadAll()));
this.isLoading = false;
}
@@ -136,8 +137,7 @@ export class AddRoleBindingDialog extends React.Component {
roleBinding: this.roleBinding,
addSubjects: subjects,
});
- }
- else {
+ } else {
const name = useRoleForBindingName ? selectedRole.getName() : bindingName;
roleBinding = await roleBindingsStore.create({ name, namespace }, {
@@ -205,7 +205,7 @@ export class AddRoleBindingDialog extends React.Component {
{
!this.useRoleForBindingName && (
this.bindingName = v}
@@ -239,7 +239,7 @@ export class AddRoleBindingDialog extends React.Component {
{
@@ -265,7 +265,7 @@ export class AddRoleBindingDialog extends React.Component {
);
const disableNext = this.isLoading || !selectedRole || !selectedBindings.length;
- const nextLabel = isEditing ? "Update" : "Create";
+ const nextLabel = isEditing ? "Update" : "Create";
return (
{
AddRoleBindingDialog.open(roleBinding)}
onRemove={selectedSubjects.length ? this.removeSelectedSubjects : null}
- addTooltip="Add bindings to {name}"
- removeTooltip="Remove selected bindings from ${name}"
+ addTooltip={`Add bindings to ${roleRef.name}`}
+ removeTooltip={`Remove selected bindings from ${roleRef.name}`}
/>
);
diff --git a/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts b/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts
index f293dea6f0..620fbd86ac 100644
--- a/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts
+++ b/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts
@@ -1,7 +1,7 @@
import difference from "lodash/difference";
import uniqBy from "lodash/uniqBy";
import { clusterRoleBindingApi, IRoleBindingSubject, RoleBinding, roleBindingApi } from "../../api/endpoints";
-import { KubeObjectStore } from "../../kube-object.store";
+import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
import { autobind } from "../../utils";
import { apiManager } from "../../api/api-manager";
@@ -9,8 +9,8 @@ import { apiManager } from "../../api/api-manager";
export class RoleBindingsStore extends KubeObjectStore {
api = clusterRoleBindingApi;
- subscribe() {
- return super.subscribe([clusterRoleBindingApi, roleBindingApi]);
+ getSubscribeApis() {
+ return [clusterRoleBindingApi, roleBindingApi];
}
protected sortItems(items: RoleBinding[]) {
@@ -26,15 +26,13 @@ export class RoleBindingsStore extends KubeObjectStore {
return clusterRoleBindingApi.get(params);
}
- protected loadItems(namespaces?: string[]) {
- if (namespaces) {
- return Promise.all(
- namespaces.map(namespace => roleBindingApi.list({ namespace }))
- ).then(items => items.flat());
- } else {
- return Promise.all([clusterRoleBindingApi.list(), roleBindingApi.list()])
- .then(items => items.flat());
- }
+ protected async loadItems(params: KubeObjectStoreLoadingParams): Promise {
+ const items = await Promise.all([
+ super.loadItems({ ...params, api: clusterRoleBindingApi }),
+ super.loadItems({ ...params, api: roleBindingApi }),
+ ]);
+
+ return items.flat();
}
protected async createItem(params: { name: string; namespace?: string }, data?: Partial) {
diff --git a/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx b/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx
index 3d64562047..6eddb7eb5a 100644
--- a/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx
+++ b/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx
@@ -10,7 +10,7 @@ import { KubeObjectListLayout } from "../kube-object";
import { AddRoleBindingDialog } from "./add-role-binding-dialog";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
bindings = "bindings",
@@ -25,13 +25,15 @@ export class RoleBindings extends React.Component {
render() {
return (
binding.getName(),
- [sortBy.namespace]: (binding: RoleBinding) => binding.getNs(),
- [sortBy.bindings]: (binding: RoleBinding) => binding.getSubjectNames(),
- [sortBy.age]: (binding: RoleBinding) => binding.metadata.creationTimestamp,
+ [columnId.name]: (binding: RoleBinding) => binding.getName(),
+ [columnId.namespace]: (binding: RoleBinding) => binding.getNs(),
+ [columnId.bindings]: (binding: RoleBinding) => binding.getSubjectNames(),
+ [columnId.age]: (binding: RoleBinding) => binding.getTimeDiffFromNow(),
}}
searchFilters={[
(binding: RoleBinding) => binding.getSearchFields(),
@@ -39,17 +41,17 @@ export class RoleBindings extends React.Component {
]}
renderHeaderTitle="Role Bindings"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Bindings", className: "bindings", sortBy: sortBy.bindings },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Bindings", className: "bindings", sortBy: columnId.bindings, id: columnId.bindings },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(binding: RoleBinding) => [
binding.getName(),
,
- binding.getSubjectNames(),
binding.getNs() || "-",
+ binding.getSubjectNames(),
binding.getAge(),
]}
addRemoveButtons={{
diff --git a/src/renderer/components/+user-management-roles/add-role-dialog.tsx b/src/renderer/components/+user-management-roles/add-role-dialog.tsx
index f421b38ba3..555a6e6c84 100644
--- a/src/renderer/components/+user-management-roles/add-role-dialog.tsx
+++ b/src/renderer/components/+user-management-roles/add-role-dialog.tsx
@@ -5,9 +5,11 @@ import { observable } from "mobx";
import { observer } from "mobx-react";
import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
+import { SubTitle } from "../layout/sub-title";
import { Notifications } from "../notifications";
import { rolesStore } from "./roles.store";
import { Input } from "../input";
+import { NamespaceSelect } from "../+namespaces/namespace-select";
import { showDetails } from "../kube-object";
interface Props extends Partial {
@@ -18,6 +20,7 @@ export class AddRoleDialog extends React.Component {
@observable static isOpen = false;
@observable roleName = "";
+ @observable namespace = "";
static open() {
AddRoleDialog.isOpen = true;
@@ -33,11 +36,12 @@ export class AddRoleDialog extends React.Component {
reset = () => {
this.roleName = "";
+ this.namespace = "";
};
createRole = async () => {
try {
- const role = await rolesStore.create({ name: this.roleName });
+ const role = await rolesStore.create({ name: this.roleName, namespace: this.namespace });
showDetails(role.selfLink);
this.reset();
@@ -64,13 +68,20 @@ export class AddRoleDialog extends React.Component {
nextLabel="Create"
next={this.createRole}
>
+
this.roleName = v}
/>
+
+ this.namespace = value}
+ />
diff --git a/src/renderer/components/+user-management-roles/roles.store.ts b/src/renderer/components/+user-management-roles/roles.store.ts
index 6af33deacb..82b0e66612 100644
--- a/src/renderer/components/+user-management-roles/roles.store.ts
+++ b/src/renderer/components/+user-management-roles/roles.store.ts
@@ -1,14 +1,14 @@
import { clusterRoleApi, Role, roleApi } from "../../api/endpoints";
import { autobind } from "../../utils";
-import { KubeObjectStore } from "../../kube-object.store";
+import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
import { apiManager } from "../../api/api-manager";
@autobind()
export class RolesStore extends KubeObjectStore {
api = clusterRoleApi;
- subscribe() {
- return super.subscribe([roleApi, clusterRoleApi]);
+ getSubscribeApis() {
+ return [roleApi, clusterRoleApi];
}
protected sortItems(items: Role[]) {
@@ -24,15 +24,13 @@ export class RolesStore extends KubeObjectStore {
return clusterRoleApi.get(params);
}
- protected loadItems(namespaces?: string[]): Promise {
- if (namespaces) {
- return Promise.all(
- namespaces.map(namespace => roleApi.list({ namespace }))
- ).then(items => items.flat());
- } else {
- return Promise.all([clusterRoleApi.list(), roleApi.list()])
- .then(items => items.flat());
- }
+ protected async loadItems(params: KubeObjectStoreLoadingParams): Promise {
+ const items = await Promise.all([
+ super.loadItems({ ...params, api: clusterRoleApi }),
+ super.loadItems({ ...params, api: roleApi }),
+ ]);
+
+ return items.flat();
}
protected async createItem(params: { name: string; namespace?: string }, data?: Partial) {
@@ -49,4 +47,4 @@ export const rolesStore = new RolesStore();
apiManager.registerStore(rolesStore, [
roleApi,
clusterRoleApi,
-]);
\ No newline at end of file
+]);
diff --git a/src/renderer/components/+user-management-roles/roles.tsx b/src/renderer/components/+user-management-roles/roles.tsx
index 21ad3bdf8a..8d916c9d42 100644
--- a/src/renderer/components/+user-management-roles/roles.tsx
+++ b/src/renderer/components/+user-management-roles/roles.tsx
@@ -10,7 +10,7 @@ import { KubeObjectListLayout } from "../kube-object";
import { AddRoleDialog } from "./add-role-dialog";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
age = "age",
@@ -25,22 +25,24 @@ export class Roles extends React.Component {
return (
<>
role.getName(),
- [sortBy.namespace]: (role: Role) => role.getNs(),
- [sortBy.age]: (role: Role) => role.metadata.creationTimestamp,
+ [columnId.name]: (role: Role) => role.getName(),
+ [columnId.namespace]: (role: Role) => role.getNs(),
+ [columnId.age]: (role: Role) => role.getTimeDiffFromNow(),
}}
searchFilters={[
(role: Role) => role.getSearchFields(),
]}
renderHeaderTitle="Roles"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(role: Role) => [
role.getName(),
diff --git a/src/renderer/components/+user-management-service-accounts/create-service-account-dialog.tsx b/src/renderer/components/+user-management-service-accounts/create-service-account-dialog.tsx
index c56888f6f4..e9a27979cf 100644
--- a/src/renderer/components/+user-management-service-accounts/create-service-account-dialog.tsx
+++ b/src/renderer/components/+user-management-service-accounts/create-service-account-dialog.tsx
@@ -66,7 +66,7 @@ export class CreateServiceAccountDialog extends React.Component {
this.name = v.toLowerCase()}
/>
diff --git a/src/renderer/components/+user-management-service-accounts/index.ts b/src/renderer/components/+user-management-service-accounts/index.ts
index fd45e28288..bd81292bf1 100644
--- a/src/renderer/components/+user-management-service-accounts/index.ts
+++ b/src/renderer/components/+user-management-service-accounts/index.ts
@@ -1,3 +1,3 @@
export * from "./service-accounts";
export * from "./service-accounts-details";
-export * from "./create-service-account-dialog";
\ No newline at end of file
+export * from "./create-service-account-dialog";
diff --git a/src/renderer/components/+user-management-service-accounts/service-accounts.tsx b/src/renderer/components/+user-management-service-accounts/service-accounts.tsx
index 37bed40ba9..34b3fc8d0a 100644
--- a/src/renderer/components/+user-management-service-accounts/service-accounts.tsx
+++ b/src/renderer/components/+user-management-service-accounts/service-accounts.tsx
@@ -15,7 +15,7 @@ import { CreateServiceAccountDialog } from "./create-service-account-dialog";
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
age = "age",
@@ -30,21 +30,23 @@ export class ServiceAccounts extends React.Component {
return (
<>
account.getName(),
- [sortBy.namespace]: (account: ServiceAccount) => account.getNs(),
- [sortBy.age]: (account: ServiceAccount) => account.metadata.creationTimestamp,
+ [columnId.name]: (account: ServiceAccount) => account.getName(),
+ [columnId.namespace]: (account: ServiceAccount) => account.getNs(),
+ [columnId.age]: (account: ServiceAccount) => account.getTimeDiffFromNow(),
}}
searchFilters={[
(account: ServiceAccount) => account.getSearchFields(),
]}
renderHeaderTitle="Service Accounts"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(account: ServiceAccount) => [
account.getName(),
diff --git a/src/renderer/components/+user-management/index.ts b/src/renderer/components/+user-management/index.ts
index 6f29869b9b..4ff825df97 100644
--- a/src/renderer/components/+user-management/index.ts
+++ b/src/renderer/components/+user-management/index.ts
@@ -1,2 +1,2 @@
export * from "./user-management";
-export * from "./user-management.route";
\ No newline at end of file
+export * from "./user-management.route";
diff --git a/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx b/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx
index 6fd04ffe7e..1b473e90c1 100644
--- a/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx
+++ b/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx
@@ -20,9 +20,7 @@ interface Props extends KubeObjectDetailsProps {
@observer
export class CronJobDetails extends React.Component {
async componentDidMount() {
- if (!jobStore.isLoaded) {
- jobStore.loadAll();
- }
+ jobStore.reloadAll();
}
render() {
diff --git a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx
index 19d35e4b3a..44c87cfba3 100644
--- a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx
+++ b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx
@@ -18,12 +18,13 @@ import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { ConfirmDialog } from "../confirm-dialog/confirm-dialog";
import { Notifications } from "../notifications/notifications";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
+ schedule = "schedule",
suspend = "suspend",
active = "active",
- lastSchedule = "schedule",
+ lastSchedule = "last-schedule",
age = "age",
}
@@ -35,15 +36,17 @@ export class CronJobs extends React.Component {
render() {
return (
cronJob.getName(),
- [sortBy.namespace]: (cronJob: CronJob) => cronJob.getNs(),
- [sortBy.suspend]: (cronJob: CronJob) => cronJob.getSuspendFlag(),
- [sortBy.active]: (cronJob: CronJob) => cronJobStore.getActiveJobsNum(cronJob),
- [sortBy.lastSchedule]: (cronJob: CronJob) => cronJob.getLastScheduleTime(),
- [sortBy.age]: (cronJob: CronJob) => cronJob.metadata.creationTimestamp,
+ [columnId.name]: (cronJob: CronJob) => cronJob.getName(),
+ [columnId.namespace]: (cronJob: CronJob) => cronJob.getNs(),
+ [columnId.suspend]: (cronJob: CronJob) => cronJob.getSuspendFlag(),
+ [columnId.active]: (cronJob: CronJob) => cronJobStore.getActiveJobsNum(cronJob),
+ [columnId.lastSchedule]: (cronJob: CronJob) => cronJob.getLastScheduleTime(),
+ [columnId.age]: (cronJob: CronJob) => cronJob.getTimeDiffFromNow(),
}}
searchFilters={[
(cronJob: CronJob) => cronJob.getSearchFields(),
@@ -51,14 +54,14 @@ export class CronJobs extends React.Component {
]}
renderHeaderTitle="Cron Jobs"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Schedule", className: "schedule" },
- { title: "Suspend", className: "suspend", sortBy: sortBy.suspend },
- { title: "Active", className: "active", sortBy: sortBy.active },
- { title: "Last schedule", className: "last-schedule", sortBy: sortBy.lastSchedule },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Schedule", className: "schedule", id: columnId.schedule },
+ { title: "Suspend", className: "suspend", sortBy: columnId.suspend, id: columnId.suspend },
+ { title: "Active", className: "active", sortBy: columnId.active, id: columnId.active },
+ { title: "Last schedule", className: "last-schedule", sortBy: columnId.lastSchedule, id: columnId.lastSchedule },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(cronJob: CronJob) => [
cronJob.getName(),
@@ -84,7 +87,7 @@ export function CronJobMenu(props: KubeObjectMenuProps) {
return (
<>
CronJobTriggerDialog.open(object)}>
-
+
Trigger
@@ -103,7 +106,7 @@ export function CronJobMenu(props: KubeObjectMenuProps) {
Resume CronJob {object.getName()} ?
),
})}>
-
+
Resume
@@ -121,7 +124,7 @@ export function CronJobMenu(props: KubeObjectMenuProps) {
Suspend CronJob {object.getName()} ?
),
})}>
-
+
Suspend
}
diff --git a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx
index 44ccbaac56..329eaf3ed7 100644
--- a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx
+++ b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx
@@ -30,9 +30,7 @@ export class DaemonSetDetails extends React.Component {
});
componentDidMount() {
- if (!podsStore.isLoaded) {
- podsStore.loadAll();
- }
+ podsStore.reloadAll();
}
componentWillUnmount() {
diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts b/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts
index ad9713e96b..b8c8dee573 100644
--- a/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts
+++ b/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts
@@ -18,7 +18,7 @@ export class DaemonSetStore extends KubeObjectStore {
}
getChildPods(daemonSet: DaemonSet): Pod[] {
- return podsStore.getPodsByOwner(daemonSet);
+ return podsStore.getPodsByOwnerId(daemonSet.getId());
}
getStatuses(daemonSets?: DaemonSet[]) {
diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx
index ff061f7877..866d561036 100644
--- a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx
+++ b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx
@@ -13,10 +13,11 @@ import { IDaemonSetsRouteParams } from "../+workloads";
import { Badge } from "../badge";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
pods = "pods",
+ labels = "labels",
age = "age",
}
@@ -38,13 +39,15 @@ export class DaemonSets extends React.Component {
render() {
return (
daemonSet.getName(),
- [sortBy.namespace]: (daemonSet: DaemonSet) => daemonSet.getNs(),
- [sortBy.pods]: (daemonSet: DaemonSet) => this.getPodsLength(daemonSet),
- [sortBy.age]: (daemonSet: DaemonSet) => daemonSet.metadata.creationTimestamp,
+ [columnId.name]: (daemonSet: DaemonSet) => daemonSet.getName(),
+ [columnId.namespace]: (daemonSet: DaemonSet) => daemonSet.getNs(),
+ [columnId.pods]: (daemonSet: DaemonSet) => this.getPodsLength(daemonSet),
+ [columnId.age]: (daemonSet: DaemonSet) => daemonSet.getTimeDiffFromNow(),
}}
searchFilters={[
(daemonSet: DaemonSet) => daemonSet.getSearchFields(),
@@ -52,12 +55,12 @@ export class DaemonSets extends React.Component {
]}
renderHeaderTitle="Daemon Sets"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Pods", className: "pods", sortBy: sortBy.pods },
- { className: "warning" },
- { title: "Node Selector", className: "labels" },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Pods", className: "pods", sortBy: columnId.pods, id: columnId.pods },
+ { className: "warning", showWithColumn: columnId.pods },
+ { title: "Node Selector", className: "labels", id: columnId.labels },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(daemonSet: DaemonSet) => [
daemonSet.getName(),
diff --git a/src/renderer/components/+workloads-deployments/deployment-details.tsx b/src/renderer/components/+workloads-deployments/deployment-details.tsx
index 26cd4c0b23..e31f63d7d7 100644
--- a/src/renderer/components/+workloads-deployments/deployment-details.tsx
+++ b/src/renderer/components/+workloads-deployments/deployment-details.tsx
@@ -31,9 +31,7 @@ export class DeploymentDetails extends React.Component {
});
componentDidMount() {
- if (!podsStore.isLoaded) {
- podsStore.loadAll();
- }
+ podsStore.reloadAll();
}
componentWillUnmount() {
diff --git a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx
index e3d18669f9..da3ac55e07 100644
--- a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx
+++ b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx
@@ -4,9 +4,9 @@ import "@testing-library/jest-dom/extend-expect";
import { DeploymentScaleDialog } from "./deployment-scale-dialog";
jest.mock("../../api/endpoints");
-import { deploymentApi } from "../../api/endpoints";
+import { Deployment, deploymentApi } from "../../api/endpoints";
-const dummyDeployment = {
+const dummyDeployment: Deployment = {
apiVersion: "v1",
kind: "dummy",
metadata: {
@@ -83,6 +83,7 @@ const dummyDeployment = {
getName: jest.fn(),
getNs: jest.fn(),
getAge: jest.fn(),
+ getTimeDiffFromNow: jest.fn(),
getFinalizers: jest.fn(),
getLabels: jest.fn(),
getAnnotations: jest.fn(),
diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx
index b84cd7b340..a2510327ee 100644
--- a/src/renderer/components/+workloads-deployments/deployments.tsx
+++ b/src/renderer/components/+workloads-deployments/deployments.tsx
@@ -23,9 +23,10 @@ import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-obje
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { Notifications } from "../notifications";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
+ pods = "pods",
replicas = "replicas",
age = "age",
condition = "condition",
@@ -55,14 +56,16 @@ export class Deployments extends React.Component {
render() {
return (
deployment.getName(),
- [sortBy.namespace]: (deployment: Deployment) => deployment.getNs(),
- [sortBy.replicas]: (deployment: Deployment) => deployment.getReplicas(),
- [sortBy.age]: (deployment: Deployment) => deployment.metadata.creationTimestamp,
- [sortBy.condition]: (deployment: Deployment) => deployment.getConditionsText(),
+ [columnId.name]: (deployment: Deployment) => deployment.getName(),
+ [columnId.namespace]: (deployment: Deployment) => deployment.getNs(),
+ [columnId.replicas]: (deployment: Deployment) => deployment.getReplicas(),
+ [columnId.age]: (deployment: Deployment) => deployment.getTimeDiffFromNow(),
+ [columnId.condition]: (deployment: Deployment) => deployment.getConditionsText(),
}}
searchFilters={[
(deployment: Deployment) => deployment.getSearchFields(),
@@ -70,13 +73,13 @@ export class Deployments extends React.Component {
]}
renderHeaderTitle="Deployments"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Pods", className: "pods" },
- { title: "Replicas", className: "replicas", sortBy: sortBy.replicas },
- { title: "Age", className: "age", sortBy: sortBy.age },
- { title: "Conditions", className: "conditions", sortBy: sortBy.condition },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Pods", className: "pods", id: columnId.pods },
+ { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
+ { title: "Conditions", className: "conditions", sortBy: columnId.condition, id: columnId.condition },
]}
renderTableContents={(deployment: Deployment) => [
deployment.getName(),
@@ -101,7 +104,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps) {
return (
<>
DeploymentScaleDialog.open(object)}>
-
+
Scale
ConfirmDialog.open({
@@ -123,7 +126,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps) {
),
})}>
-
+
Restart
>
diff --git a/src/renderer/components/+workloads-jobs/job-details.tsx b/src/renderer/components/+workloads-jobs/job-details.tsx
index dfa16fe760..f0665bd291 100644
--- a/src/renderer/components/+workloads-jobs/job-details.tsx
+++ b/src/renderer/components/+workloads-jobs/job-details.tsx
@@ -25,9 +25,7 @@ interface Props extends KubeObjectDetailsProps {
@observer
export class JobDetails extends React.Component {
async componentDidMount() {
- if (!podsStore.isLoaded) {
- podsStore.loadAll();
- }
+ podsStore.reloadAll();
}
render() {
diff --git a/src/renderer/components/+workloads-jobs/job.store.ts b/src/renderer/components/+workloads-jobs/job.store.ts
index 569c9efb13..41d514df8d 100644
--- a/src/renderer/components/+workloads-jobs/job.store.ts
+++ b/src/renderer/components/+workloads-jobs/job.store.ts
@@ -10,7 +10,7 @@ export class JobStore extends KubeObjectStore {
api = jobApi;
getChildPods(job: Job): Pod[] {
- return podsStore.getPodsByOwner(job);
+ return podsStore.getPodsByOwnerId(job.getId());
}
getJobsByOwner(cronJob: CronJob) {
diff --git a/src/renderer/components/+workloads-jobs/jobs.tsx b/src/renderer/components/+workloads-jobs/jobs.tsx
index 00c1ee0db5..5961c17376 100644
--- a/src/renderer/components/+workloads-jobs/jobs.tsx
+++ b/src/renderer/components/+workloads-jobs/jobs.tsx
@@ -12,9 +12,10 @@ import { IJobsRouteParams } from "../+workloads";
import kebabCase from "lodash/kebabCase";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
+ completions = "completions",
conditions = "conditions",
age = "age",
}
@@ -27,25 +28,27 @@ export class Jobs extends React.Component {
render() {
return (
job.getName(),
- [sortBy.namespace]: (job: Job) => job.getNs(),
- [sortBy.conditions]: (job: Job) => job.getCondition() != null ? job.getCondition().type : "",
- [sortBy.age]: (job: Job) => job.metadata.creationTimestamp,
+ [columnId.name]: (job: Job) => job.getName(),
+ [columnId.namespace]: (job: Job) => job.getNs(),
+ [columnId.conditions]: (job: Job) => job.getCondition() != null ? job.getCondition().type : "",
+ [columnId.age]: (job: Job) => job.getTimeDiffFromNow(),
}}
searchFilters={[
(job: Job) => job.getSearchFields(),
]}
renderHeaderTitle="Jobs"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Completions", className: "completions" },
- { className: "warning" },
- { title: "Age", className: "age", sortBy: sortBy.age },
- { title: "Conditions", className: "conditions", sortBy: sortBy.conditions },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Completions", className: "completions", id: columnId.completions },
+ { className: "warning", showWithColumn: columnId.completions },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
+ { title: "Conditions", className: "conditions", sortBy: columnId.conditions, id: columnId.conditions },
]}
renderTableContents={(job: Job) => {
const condition = job.getCondition();
diff --git a/src/renderer/components/+workloads-overview/overview-statuses.tsx b/src/renderer/components/+workloads-overview/overview-statuses.tsx
index 78adecb6df..bc0484dadc 100644
--- a/src/renderer/components/+workloads-overview/overview-statuses.tsx
+++ b/src/renderer/components/+workloads-overview/overview-statuses.tsx
@@ -6,10 +6,9 @@ import { OverviewWorkloadStatus } from "./overview-workload-status";
import { Link } from "react-router-dom";
import { workloadURL, workloadStores } from "../+workloads";
import { namespaceStore } from "../+namespaces/namespace.store";
-import { PageFiltersList } from "../item-object-list/page-filters-list";
-import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
+import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
import { isAllowedResource, KubeResource } from "../../../common/rbac";
-import { ResourceNames } from "../../../renderer/utils/rbac";
+import { ResourceNames } from "../../utils/rbac";
import { autobind } from "../../utils";
const resources: KubeResource[] = [
@@ -27,7 +26,7 @@ export class OverviewStatuses extends React.Component {
@autobind()
renderWorkload(resource: KubeResource): React.ReactElement {
const store = workloadStores[resource];
- const items = store.getAllByNs(namespaceStore.contextNs);
+ const items = store.getAllByNs(namespaceStore.contextNamespaces);
return (
@@ -50,7 +49,6 @@ export class OverviewStatuses extends React.Component {
Overview
-
{workloads}
diff --git a/src/renderer/components/+workloads-overview/overview.tsx b/src/renderer/components/+workloads-overview/overview.tsx
index 318ad53f77..92bc569307 100644
--- a/src/renderer/components/+workloads-overview/overview.tsx
+++ b/src/renderer/components/+workloads-overview/overview.tsx
@@ -1,8 +1,7 @@
import "./overview.scss";
import React from "react";
-import { observable, when } from "mobx";
-import { observer } from "mobx-react";
+import { disposeOnUnmount, observer } from "mobx-react";
import { OverviewStatuses } from "./overview-statuses";
import { RouteComponentProps } from "react-router";
import { IWorkloadsOverviewRouteParams } from "../+workloads";
@@ -15,83 +14,32 @@ import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
import { jobStore } from "../+workloads-jobs/job.store";
import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
import { Events } from "../+events";
-import { KubeObjectStore } from "../../kube-object.store";
import { isAllowedResource } from "../../../common/rbac";
+import { kubeWatchApi } from "../../api/kube-watch-api";
+import { clusterContext } from "../context";
interface Props extends RouteComponentProps {
}
@observer
export class WorkloadsOverview extends React.Component {
- @observable isUnmounting = false;
-
- async componentDidMount() {
- const stores: KubeObjectStore[] = [];
-
- if (isAllowedResource("pods")) {
- stores.push(podsStore);
- }
-
- if (isAllowedResource("deployments")) {
- stores.push(deploymentStore);
- }
-
- if (isAllowedResource("daemonsets")) {
- stores.push(daemonSetStore);
- }
-
- if (isAllowedResource("statefulsets")) {
- stores.push(statefulSetStore);
- }
-
- if (isAllowedResource("replicasets")) {
- stores.push(replicaSetStore);
- }
-
- if (isAllowedResource("jobs")) {
- stores.push(jobStore);
- }
-
- if (isAllowedResource("cronjobs")) {
- stores.push(cronJobStore);
- }
-
- if (isAllowedResource("events")) {
- stores.push(eventStore);
- }
-
- const unsubscribeList: Array<() => void> = [];
-
- for (const store of stores) {
- await store.loadAll();
- unsubscribeList.push(store.subscribe());
- }
-
- await when(() => this.isUnmounting);
- unsubscribeList.forEach(dispose => dispose());
- }
-
- componentWillUnmount() {
- this.isUnmounting = true;
- }
-
- get contents() {
- return (
- <>
-
- { isAllowedResource("events") && }
- >
- );
+ componentDidMount() {
+ disposeOnUnmount(this, [
+ kubeWatchApi.subscribeStores([
+ podsStore, deploymentStore, daemonSetStore, statefulSetStore, replicaSetStore,
+ jobStore, cronJobStore, eventStore,
+ ], {
+ preload: true,
+ namespaces: clusterContext.contextNamespaces,
+ }),
+ ]);
}
render() {
return (
- {this.contents}
+
+ {isAllowedResource("events") && }
);
}
diff --git a/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx
new file mode 100644
index 0000000000..dbde813e5a
--- /dev/null
+++ b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx
@@ -0,0 +1,59 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import React from "react";
+import "@testing-library/jest-dom/extend-expect";
+import { fireEvent, render } from "@testing-library/react";
+import { IToleration } from "../../../api/workload-kube-object";
+import { PodTolerations } from "../pod-tolerations";
+
+const tolerations: IToleration[] =[
+ {
+ key: "CriticalAddonsOnly",
+ operator: "Exist",
+ effect: "NoExecute",
+ tolerationSeconds: 3600
+ },
+ {
+ key: "node.kubernetes.io/not-ready",
+ operator: "NoExist",
+ effect: "NoSchedule",
+ tolerationSeconds: 7200
+ },
+];
+
+describe(" ", () => {
+ it("renders w/o errors", () => {
+ const { container } = render( );
+
+ expect(container).toBeInstanceOf(HTMLElement);
+ });
+
+ it("shows all tolerations", () => {
+ const { container } = render( );
+ const rows = container.querySelectorAll(".TableRow");
+
+ expect(rows[0].querySelector(".key").textContent).toBe("CriticalAddonsOnly");
+ expect(rows[0].querySelector(".operator").textContent).toBe("Exist");
+ expect(rows[0].querySelector(".effect").textContent).toBe("NoExecute");
+ expect(rows[0].querySelector(".seconds").textContent).toBe("3600");
+
+ expect(rows[1].querySelector(".key").textContent).toBe("node.kubernetes.io/not-ready");
+ expect(rows[1].querySelector(".operator").textContent).toBe("NoExist");
+ expect(rows[1].querySelector(".effect").textContent).toBe("NoSchedule");
+ expect(rows[1].querySelector(".seconds").textContent).toBe("7200");
+ });
+
+ it("sorts table properly", () => {
+ const { container, getByText } = render( );
+ const headCell = getByText("Key");
+
+ fireEvent.click(headCell);
+ fireEvent.click(headCell);
+
+ const rows = container.querySelectorAll(".TableRow");
+
+ expect(rows[0].querySelector(".key").textContent).toBe("node.kubernetes.io/not-ready");
+ });
+});
diff --git a/src/renderer/components/+workloads-pods/index.ts b/src/renderer/components/+workloads-pods/index.ts
index f3181cb3a2..cc7782911c 100644
--- a/src/renderer/components/+workloads-pods/index.ts
+++ b/src/renderer/components/+workloads-pods/index.ts
@@ -1,2 +1,2 @@
export * from "./pods";
-export * from "./pod-details";
\ No newline at end of file
+export * from "./pod-details";
diff --git a/src/renderer/components/+workloads-pods/pod-container-env.tsx b/src/renderer/components/+workloads-pods/pod-container-env.tsx
index 38af50a457..2e96b142f9 100644
--- a/src/renderer/components/+workloads-pods/pod-container-env.tsx
+++ b/src/renderer/components/+workloads-pods/pod-container-env.tsx
@@ -30,7 +30,11 @@ export const ContainerEnvironment = observer((props: Props) => {
}
});
envFrom && envFrom.forEach(item => {
- const { configMapRef } = item;
+ const { configMapRef, secretRef } = item;
+
+ if (secretRef && secretRef.name) {
+ secretsStore.load({ name: secretRef.name, namespace });
+ }
if (configMapRef && configMapRef.name) {
configMapsStore.load({ name: configMapRef.name, namespace });
@@ -89,21 +93,54 @@ export const ContainerEnvironment = observer((props: Props) => {
const renderEnvFrom = () => {
const envVars = envFrom.map(vars => {
- if (!vars.configMapRef || !vars.configMapRef.name) return;
- const configMap = configMapsStore.getByName(vars.configMapRef.name, namespace);
-
- if (!configMap) return;
-
- return Object.entries(configMap.data).map(([name, value]) => (
-
- {name} : {value}
-
- ));
+ if (vars.configMapRef?.name) {
+ return renderEnvFromConfigMap(vars.configMapRef.name);
+ } else if (vars.secretRef?.name ) {
+ return renderEnvFromSecret(vars.secretRef.name);
+ }
});
return _.flatten(envVars);
};
+ const renderEnvFromConfigMap = (configMapName: string) => {
+ const configMap = configMapsStore.getByName(configMapName, namespace);
+
+ if (!configMap) return;
+
+ return Object.entries(configMap.data).map(([name, value]) => (
+
+ {name} : {value}
+
+ ));
+ };
+
+ const renderEnvFromSecret = (secretName: string) => {
+ const secret = secretsStore.getByName(secretName, namespace);
+
+ if (!secret) return;
+
+ return Object.keys(secret.data).map(key => {
+ const secretKeyRef = {
+ name: secret.getName(),
+ key
+ };
+
+ const value = (
+
+ );
+
+ return (
+
+ {key} : {value}
+
+ );
+ });
+ };
+
return (
{env && renderEnv()}
diff --git a/src/renderer/components/+workloads-pods/pod-container-port.tsx b/src/renderer/components/+workloads-pods/pod-container-port.tsx
index ce5c27c462..4f2124ec3d 100644
--- a/src/renderer/components/+workloads-pods/pod-container-port.tsx
+++ b/src/renderer/components/+workloads-pods/pod-container-port.tsx
@@ -43,7 +43,7 @@ export class PodContainerPort extends React.Component {
return (
-
this.portForward() }>
+ this.portForward() }>
{text}
{this.waiting && (
diff --git a/src/renderer/components/+workloads-pods/pod-details-statuses.tsx b/src/renderer/components/+workloads-pods/pod-details-statuses.tsx
index 5ce8465e72..1e0f765381 100644
--- a/src/renderer/components/+workloads-pods/pod-details-statuses.tsx
+++ b/src/renderer/components/+workloads-pods/pod-details-statuses.tsx
@@ -27,4 +27,4 @@ export class PodDetailsStatuses extends React.Component {
);
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/+workloads-pods/pod-details-tolerations.scss b/src/renderer/components/+workloads-pods/pod-details-tolerations.scss
index 0aa68fa1d6..1ac932cd9d 100644
--- a/src/renderer/components/+workloads-pods/pod-details-tolerations.scss
+++ b/src/renderer/components/+workloads-pods/pod-details-tolerations.scss
@@ -1,5 +1,23 @@
.PodDetailsTolerations {
- .toleration {
- margin-bottom: $margin;
+ grid-template-columns: auto;
+
+ .PodTolerations {
+ margin-top: var(--margin);
+ }
+
+ // Expanding value cell to cover 2 columns (whole Drawer width)
+
+ > .name {
+ grid-row-start: 1;
+ grid-column-start: 1;
+ }
+
+ > .value {
+ grid-row-start: 1;
+ grid-column-start: 1;
+ }
+
+ .DrawerParamToggler > .params {
+ margin-left: var(--drawer-item-title-width);
}
}
\ No newline at end of file
diff --git a/src/renderer/components/+workloads-pods/pod-details-tolerations.tsx b/src/renderer/components/+workloads-pods/pod-details-tolerations.tsx
index 8b67502e26..67bd5a07d0 100644
--- a/src/renderer/components/+workloads-pods/pod-details-tolerations.tsx
+++ b/src/renderer/components/+workloads-pods/pod-details-tolerations.tsx
@@ -1,10 +1,11 @@
import "./pod-details-tolerations.scss";
import React from "react";
-import { Pod, Deployment, DaemonSet, StatefulSet, ReplicaSet, Job } from "../../api/endpoints";
import { DrawerParamToggler, DrawerItem } from "../drawer";
+import { WorkloadKubeObject } from "../../api/workload-kube-object";
+import { PodTolerations } from "./pod-tolerations";
interface Props {
- workload: Pod | Deployment | DaemonSet | StatefulSet | ReplicaSet | Job;
+ workload: WorkloadKubeObject;
}
export class PodDetailsTolerations extends React.Component {
@@ -17,20 +18,7 @@ export class PodDetailsTolerations extends React.Component {
return (
- {
- tolerations.map((toleration, index) => {
- const { key, operator, effect, tolerationSeconds } = toleration;
-
- return (
-
- {key}
- {operator && {operator} }
- {effect && {effect} }
- {!!tolerationSeconds && {tolerationSeconds} }
-
- );
- })
- }
+
);
diff --git a/src/renderer/components/+workloads-pods/pod-tolerations.scss b/src/renderer/components/+workloads-pods/pod-tolerations.scss
new file mode 100644
index 0000000000..b840697685
--- /dev/null
+++ b/src/renderer/components/+workloads-pods/pod-tolerations.scss
@@ -0,0 +1,14 @@
+.PodTolerations {
+ .TableHead {
+ background-color: var(--drawerSubtitleBackground);
+ }
+
+ .TableCell {
+ white-space: normal;
+ word-break: normal;
+
+ &.key {
+ flex-grow: 3;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/components/+workloads-pods/pod-tolerations.tsx b/src/renderer/components/+workloads-pods/pod-tolerations.tsx
new file mode 100644
index 0000000000..e8d3d7d099
--- /dev/null
+++ b/src/renderer/components/+workloads-pods/pod-tolerations.tsx
@@ -0,0 +1,63 @@
+import "./pod-tolerations.scss";
+import React from "react";
+import uniqueId from "lodash/uniqueId";
+
+import { IToleration } from "../../api/workload-kube-object";
+import { Table, TableCell, TableHead, TableRow } from "../table";
+
+interface Props {
+ tolerations: IToleration[];
+}
+
+enum sortBy {
+ Key = "key",
+ Operator = "operator",
+ Effect = "effect",
+ Seconds = "seconds",
+}
+
+const sortingCallbacks = {
+ [sortBy.Key]: (toleration: IToleration) => toleration.key,
+ [sortBy.Operator]: (toleration: IToleration) => toleration.operator,
+ [sortBy.Effect]: (toleration: IToleration) => toleration.effect,
+ [sortBy.Seconds]: (toleration: IToleration) => toleration.tolerationSeconds,
+};
+
+const getTableRow = (toleration: IToleration) => {
+ const { key, operator, effect, tolerationSeconds } = toleration;
+
+ return (
+
+ {key}
+ {operator}
+ {effect}
+ {tolerationSeconds}
+
+ );
+};
+
+export function PodTolerations({ tolerations }: Props) {
+ return (
+
+
+ Key
+ Operator
+ Effect
+ Seconds
+
+ {
+ tolerations.map(getTableRow)
+ }
+
+ );
+}
diff --git a/src/renderer/components/+workloads-pods/pods.store.ts b/src/renderer/components/+workloads-pods/pods.store.ts
index 9cd3c3b2f9..5a535cec66 100644
--- a/src/renderer/components/+workloads-pods/pods.store.ts
+++ b/src/renderer/components/+workloads-pods/pods.store.ts
@@ -3,8 +3,8 @@ import { action, observable } from "mobx";
import { KubeObjectStore } from "../../kube-object.store";
import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils";
import { IPodMetrics, Pod, PodMetrics, podMetricsApi, podsApi } from "../../api/endpoints";
-import { WorkloadKubeObject } from "../../api/workload-kube-object";
import { apiManager } from "../../api/api-manager";
+import { WorkloadKubeObject } from "../../api/workload-kube-object";
@autobind()
export class PodsStore extends KubeObjectStore {
@@ -44,6 +44,12 @@ export class PodsStore extends KubeObjectStore {
});
}
+ getPodsByOwnerId(workloadId: string): Pod[] {
+ return this.items.filter(pod => {
+ return pod.getOwnerRefs().find(owner => owner.uid === workloadId);
+ });
+ }
+
getPodsByNode(node: string) {
if (!this.isLoaded) return [];
diff --git a/src/renderer/components/+workloads-pods/pods.tsx b/src/renderer/components/+workloads-pods/pods.tsx
index 88981b968f..fc2a0930af 100644
--- a/src/renderer/components/+workloads-pods/pods.tsx
+++ b/src/renderer/components/+workloads-pods/pods.tsx
@@ -19,8 +19,7 @@ import { lookupApiLink } from "../../api/kube-api";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { Badge } from "../badge";
-
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
containers = "containers",
@@ -74,16 +73,18 @@ export class Pods extends React.Component {
pod.getName(),
- [sortBy.namespace]: (pod: Pod) => pod.getNs(),
- [sortBy.containers]: (pod: Pod) => pod.getContainers().length,
- [sortBy.restarts]: (pod: Pod) => pod.getRestartsCount(),
- [sortBy.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind),
- [sortBy.qos]: (pod: Pod) => pod.getQosClass(),
- [sortBy.node]: (pod: Pod) => pod.getNodeName(),
- [sortBy.age]: (pod: Pod) => pod.metadata.creationTimestamp,
- [sortBy.status]: (pod: Pod) => pod.getStatusMessage(),
+ [columnId.name]: (pod: Pod) => pod.getName(),
+ [columnId.namespace]: (pod: Pod) => pod.getNs(),
+ [columnId.containers]: (pod: Pod) => pod.getContainers().length,
+ [columnId.restarts]: (pod: Pod) => pod.getRestartsCount(),
+ [columnId.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind),
+ [columnId.qos]: (pod: Pod) => pod.getQosClass(),
+ [columnId.node]: (pod: Pod) => pod.getNodeName(),
+ [columnId.age]: (pod: Pod) => pod.getTimeDiffFromNow(),
+ [columnId.status]: (pod: Pod) => pod.getStatusMessage(),
}}
searchFilters={[
(pod: Pod) => pod.getSearchFields(),
@@ -93,16 +94,16 @@ export class Pods extends React.Component {
]}
renderHeaderTitle="Pods"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Containers", className: "containers", sortBy: sortBy.containers },
- { title: "Restarts", className: "restarts", sortBy: sortBy.restarts },
- { title: "Controlled By", className: "owners", sortBy: sortBy.owners },
- { title: "Node", className: "node", sortBy: sortBy.node },
- { title: "QoS", className: "qos", sortBy: sortBy.qos },
- { title: "Age", className: "age", sortBy: sortBy.age },
- { title: "Status", className: "status", sortBy: sortBy.status },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Containers", className: "containers", sortBy: columnId.containers, id: columnId.containers },
+ { title: "Restarts", className: "restarts", sortBy: columnId.restarts, id: columnId.restarts },
+ { title: "Controlled By", className: "owners", sortBy: columnId.owners, id: columnId.owners },
+ { title: "Node", className: "node", sortBy: columnId.node, id: columnId.node },
+ { title: "QoS", className: "qos", sortBy: columnId.qos, id: columnId.qos },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
+ { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
]}
renderTableContents={(pod: Pod) => [
,
diff --git a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx
index 8c28d81a83..0cf747a1d1 100644
--- a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx
+++ b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx
@@ -29,9 +29,7 @@ export class ReplicaSetDetails extends React.Component {
});
async componentDidMount() {
- if (!podsStore.isLoaded) {
- podsStore.loadAll();
- }
+ podsStore.reloadAll();
}
componentWillUnmount() {
diff --git a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx
index 804b7c344f..131bc08ca2 100755
--- a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx
+++ b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx
@@ -4,9 +4,9 @@ jest.mock("../../api/endpoints");
import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog";
import { render, waitFor, fireEvent } from "@testing-library/react";
import React from "react";
-import { replicaSetApi } from "../../api/endpoints/replica-set.api";
+import { ReplicaSet, replicaSetApi } from "../../api/endpoints/replica-set.api";
-const dummyReplicaSet = {
+const dummyReplicaSet: ReplicaSet = {
apiVersion: "v1",
kind: "dummy",
metadata: {
@@ -67,7 +67,6 @@ const dummyReplicaSet = {
getCurrent: jest.fn(),
getReady: jest.fn(),
getImages: jest.fn(),
- getReplicas: jest.fn(),
getSelectors: jest.fn(),
getTemplateLabels: jest.fn(),
getAffinity: jest.fn(),
@@ -79,6 +78,7 @@ const dummyReplicaSet = {
getName: jest.fn(),
getNs: jest.fn(),
getAge: jest.fn(),
+ getTimeDiffFromNow: jest.fn(),
getFinalizers: jest.fn(),
getLabels: jest.fn(),
getAnnotations: jest.fn(),
diff --git a/src/renderer/components/+workloads-replicasets/replicasets.store.ts b/src/renderer/components/+workloads-replicasets/replicasets.store.ts
index 337f9c0ae1..ca58006930 100644
--- a/src/renderer/components/+workloads-replicasets/replicasets.store.ts
+++ b/src/renderer/components/+workloads-replicasets/replicasets.store.ts
@@ -18,7 +18,7 @@ export class ReplicaSetStore extends KubeObjectStore {
}
getChildPods(replicaSet: ReplicaSet) {
- return podsStore.getPodsByOwner(replicaSet);
+ return podsStore.getPodsByOwnerId(replicaSet.getId());
}
getStatuses(replicaSets: ReplicaSet[]) {
diff --git a/src/renderer/components/+workloads-replicasets/replicasets.tsx b/src/renderer/components/+workloads-replicasets/replicasets.tsx
index 55f607e3c3..1caa394df6 100644
--- a/src/renderer/components/+workloads-replicasets/replicasets.tsx
+++ b/src/renderer/components/+workloads-replicasets/replicasets.tsx
@@ -14,7 +14,7 @@ import { Icon } from "../icon/icon";
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
desired = "desired",
@@ -31,27 +31,29 @@ export class ReplicaSets extends React.Component {
render() {
return (
replicaSet.getName(),
- [sortBy.namespace]: (replicaSet: ReplicaSet) => replicaSet.getNs(),
- [sortBy.desired]: (replicaSet: ReplicaSet) => replicaSet.getDesired(),
- [sortBy.current]: (replicaSet: ReplicaSet) => replicaSet.getCurrent(),
- [sortBy.ready]: (replicaSet: ReplicaSet) => replicaSet.getReady(),
- [sortBy.age]: (replicaSet: ReplicaSet) => replicaSet.metadata.creationTimestamp,
+ [columnId.name]: (replicaSet: ReplicaSet) => replicaSet.getName(),
+ [columnId.namespace]: (replicaSet: ReplicaSet) => replicaSet.getNs(),
+ [columnId.desired]: (replicaSet: ReplicaSet) => replicaSet.getDesired(),
+ [columnId.current]: (replicaSet: ReplicaSet) => replicaSet.getCurrent(),
+ [columnId.ready]: (replicaSet: ReplicaSet) => replicaSet.getReady(),
+ [columnId.age]: (replicaSet: ReplicaSet) => replicaSet.getTimeDiffFromNow(),
}}
searchFilters={[
(replicaSet: ReplicaSet) => replicaSet.getSearchFields(),
]}
renderHeaderTitle="Replica Sets"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { className: "warning" },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Desired", className: "desired", sortBy: sortBy.desired },
- { title: "Current", className: "current", sortBy: sortBy.current },
- { title: "Ready", className: "ready", sortBy: sortBy.ready },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { className: "warning", showWithColumn: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Desired", className: "desired", sortBy: columnId.desired, id: columnId.desired },
+ { title: "Current", className: "current", sortBy: columnId.current, id: columnId.current },
+ { title: "Ready", className: "ready", sortBy: columnId.ready, id: columnId.ready },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(replicaSet: ReplicaSet) => [
replicaSet.getName(),
@@ -76,7 +78,7 @@ export function ReplicaSetMenu(props: KubeObjectMenuProps) {
return (
<>
ReplicaSetScaleDialog.open(object)}>
-
+
Scale
>
diff --git a/src/renderer/components/+workloads-statefulsets/index.ts b/src/renderer/components/+workloads-statefulsets/index.ts
index 1cb72d701a..af942b604f 100644
--- a/src/renderer/components/+workloads-statefulsets/index.ts
+++ b/src/renderer/components/+workloads-statefulsets/index.ts
@@ -1,2 +1,2 @@
export * from "./statefulsets";
-export * from "./statefulset-details";
\ No newline at end of file
+export * from "./statefulset-details";
diff --git a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx
index d30633ff32..f1f86b6b5f 100644
--- a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx
+++ b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx
@@ -30,9 +30,7 @@ export class StatefulSetDetails extends React.Component {
});
componentDidMount() {
- if (!podsStore.isLoaded) {
- podsStore.loadAll();
- }
+ podsStore.reloadAll();
}
componentWillUnmount() {
diff --git a/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx b/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx
index c0a26dbd52..faf94b995d 100755
--- a/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx
+++ b/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx
@@ -1,12 +1,12 @@
import "@testing-library/jest-dom/extend-expect";
jest.mock("../../api/endpoints");
-import { statefulSetApi } from "../../api/endpoints";
+import { StatefulSet, statefulSetApi } from "../../api/endpoints";
import { StatefulSetScaleDialog } from "./statefulset-scale-dialog";
import { render, waitFor, fireEvent } from "@testing-library/react";
import React from "react";
-const dummyStatefulSet = {
+const dummyStatefulSet: StatefulSet = {
apiVersion: "v1",
kind: "dummy",
metadata: {
@@ -88,6 +88,7 @@ const dummyStatefulSet = {
getName: jest.fn(),
getNs: jest.fn(),
getAge: jest.fn(),
+ getTimeDiffFromNow: jest.fn(),
getFinalizers: jest.fn(),
getLabels: jest.fn(),
getAnnotations: jest.fn(),
diff --git a/src/renderer/components/+workloads-statefulsets/statefulset.store.ts b/src/renderer/components/+workloads-statefulsets/statefulset.store.ts
index 12f1f663b9..6ee4bb5c28 100644
--- a/src/renderer/components/+workloads-statefulsets/statefulset.store.ts
+++ b/src/renderer/components/+workloads-statefulsets/statefulset.store.ts
@@ -17,7 +17,7 @@ export class StatefulSetStore extends KubeObjectStore {
}
getChildPods(statefulSet: StatefulSet) {
- return podsStore.getPodsByOwner(statefulSet);
+ return podsStore.getPodsByOwnerId(statefulSet.getId());
}
getStatuses(statefulSets: StatefulSet[]) {
diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx
index 9e6011e156..868a6afc45 100644
--- a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx
+++ b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx
@@ -17,9 +17,10 @@ import { MenuItem } from "../menu/menu";
import { Icon } from "../icon/icon";
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
-enum sortBy {
+enum columnId {
name = "name",
namespace = "namespace",
+ pods = "pods",
age = "age",
replicas = "replicas",
}
@@ -38,25 +39,27 @@ export class StatefulSets extends React.Component {
render() {
return (
statefulSet.getName(),
- [sortBy.namespace]: (statefulSet: StatefulSet) => statefulSet.getNs(),
- [sortBy.age]: (statefulSet: StatefulSet) => statefulSet.metadata.creationTimestamp,
- [sortBy.replicas]: (statefulSet: StatefulSet) => statefulSet.getReplicas(),
+ [columnId.name]: (statefulSet: StatefulSet) => statefulSet.getName(),
+ [columnId.namespace]: (statefulSet: StatefulSet) => statefulSet.getNs(),
+ [columnId.age]: (statefulSet: StatefulSet) => statefulSet.getTimeDiffFromNow(),
+ [columnId.replicas]: (statefulSet: StatefulSet) => statefulSet.getReplicas(),
}}
searchFilters={[
(statefulSet: StatefulSet) => statefulSet.getSearchFields(),
]}
renderHeaderTitle="Stateful Sets"
renderTableHeader={[
- { title: "Name", className: "name", sortBy: sortBy.name },
- { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
- { title: "Pods", className: "pods" },
- { title: "Replicas", className: "replicas", sortBy: sortBy.replicas },
- { className: "warning" },
- { title: "Age", className: "age", sortBy: sortBy.age },
+ { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
+ { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
+ { title: "Pods", className: "pods", id: columnId.pods },
+ { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas },
+ { className: "warning", showWithColumn: columnId.replicas },
+ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(statefulSet: StatefulSet) => [
statefulSet.getName(),
@@ -80,7 +83,7 @@ export function StatefulSetMenu(props: KubeObjectMenuProps) {
return (
<>
StatefulSetScaleDialog.open(object)}>
-
+
Scale
>
diff --git a/src/renderer/components/+workloads/index.ts b/src/renderer/components/+workloads/index.ts
index a6e1904b84..7e40e91f98 100644
--- a/src/renderer/components/+workloads/index.ts
+++ b/src/renderer/components/+workloads/index.ts
@@ -1,3 +1,4 @@
export * from "./workloads.route";
export * from "./workloads";
export * from "./workloads.stores";
+export * from "./workloads.command";
diff --git a/src/renderer/components/+workloads/workloads.command.ts b/src/renderer/components/+workloads/workloads.command.ts
new file mode 100644
index 0000000000..02b5e42657
--- /dev/null
+++ b/src/renderer/components/+workloads/workloads.command.ts
@@ -0,0 +1,45 @@
+import { navigate } from "../../navigation";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { cronJobsURL, daemonSetsURL, deploymentsURL, jobsURL, podsURL, statefulSetsURL } from "./workloads.route";
+
+commandRegistry.add({
+ id: "cluster.viewPods",
+ title: "Cluster: View Pods",
+ scope: "cluster",
+ action: () => navigate(podsURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewDeployments",
+ title: "Cluster: View Deployments",
+ scope: "cluster",
+ action: () => navigate(deploymentsURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewDaemonSets",
+ title: "Cluster: View DaemonSets",
+ scope: "cluster",
+ action: () => navigate(daemonSetsURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewStatefulSets",
+ title: "Cluster: View StatefulSets",
+ scope: "cluster",
+ action: () => navigate(statefulSetsURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewJobs",
+ title: "Cluster: View Jobs",
+ scope: "cluster",
+ action: () => navigate(jobsURL())
+});
+
+commandRegistry.add({
+ id: "cluster.viewCronJobs",
+ title: "Cluster: View CronJobs",
+ scope: "cluster",
+ action: () => navigate(cronJobsURL())
+});
diff --git a/src/renderer/components/+workspaces/add-workspace.tsx b/src/renderer/components/+workspaces/add-workspace.tsx
new file mode 100644
index 0000000000..868acc0e98
--- /dev/null
+++ b/src/renderer/components/+workspaces/add-workspace.tsx
@@ -0,0 +1,64 @@
+import React from "react";
+import { observer } from "mobx-react";
+import { Workspace, workspaceStore } from "../../../common/workspace-store";
+import { v4 as uuid } from "uuid";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { Input, InputValidator } from "../input";
+import { navigate } from "../../navigation";
+import { CommandOverlay } from "../command-palette/command-container";
+import { landingURL } from "../+landing-page";
+import { clusterStore } from "../../../common/cluster-store";
+
+const uniqueWorkspaceName: InputValidator = {
+ condition: ({ required }) => required,
+ message: () => `Workspace with this name already exists`,
+ validate: value => !workspaceStore.getByName(value),
+};
+
+@observer
+export class AddWorkspace extends React.Component {
+ onSubmit(name: string) {
+ if (!name.trim()) {
+ return;
+ }
+ const workspace = workspaceStore.addWorkspace(new Workspace({
+ id: uuid(),
+ name
+ }));
+
+ if (!workspace) {
+ return;
+ }
+
+ workspaceStore.setActive(workspace.id);
+ clusterStore.setActive(null);
+ navigate(landingURL());
+ CommandOverlay.close();
+ }
+
+ render() {
+ return (
+ <>
+ this.onSubmit(v)}
+ dirty={true}
+ showValidationLine={true} />
+
+ Please provide a new workspace name (Press "Enter" to confirm or "Escape" to cancel)
+
+ >
+ );
+ }
+}
+
+commandRegistry.add({
+ id: "workspace.addWorkspace",
+ title: "Workspace: Add workspace ...",
+ scope: "global",
+ action: () => CommandOverlay.open( )
+});
diff --git a/src/renderer/components/+workspaces/edit-workspace.tsx b/src/renderer/components/+workspaces/edit-workspace.tsx
new file mode 100644
index 0000000000..3ab4b44d5a
--- /dev/null
+++ b/src/renderer/components/+workspaces/edit-workspace.tsx
@@ -0,0 +1,82 @@
+import React from "react";
+import { observer } from "mobx-react";
+import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { Input, InputValidator } from "../input";
+import { CommandOverlay } from "../command-palette/command-container";
+
+const validateWorkspaceName: InputValidator = {
+ condition: ({ required }) => required,
+ message: () => `Workspace with this name already exists`,
+ validate: (value) => {
+ const current = workspaceStore.currentWorkspace;
+
+ if (current.name === value.trim()) {
+ return true;
+ }
+
+ return !workspaceStore.enabledWorkspacesList.find((workspace) => workspace.name === value);
+ }
+};
+
+interface EditWorkspaceState {
+ name: string;
+}
+
+@observer
+export class EditWorkspace extends React.Component<{}, EditWorkspaceState> {
+
+ state: EditWorkspaceState = {
+ name: ""
+ };
+
+ componentDidMount() {
+ this.setState({name: workspaceStore.currentWorkspace.name});
+ }
+
+ onSubmit(name: string) {
+ if (name.trim() === "") {
+ return;
+ }
+
+ workspaceStore.currentWorkspace.name = name;
+ CommandOverlay.close();
+ }
+
+ onChange(name: string) {
+ this.setState({name});
+ }
+
+ get name() {
+ return this.state.name;
+ }
+
+ render() {
+ return (
+ <>
+ this.onChange(v)}
+ onSubmit={(v) => this.onSubmit(v)}
+ dirty={true}
+ value={this.name}
+ showValidationLine={true} />
+
+ Please provide a new workspace name (Press "Enter" to confirm or "Escape" to cancel)
+
+ >
+ );
+ }
+}
+
+commandRegistry.add({
+ id: "workspace.editCurrentWorkspace",
+ title: "Workspace: Edit current workspace ...",
+ scope: "global",
+ action: () => CommandOverlay.open( ),
+ isActive: (context) => context.workspace?.id !== WorkspaceStore.defaultId
+});
diff --git a/src/renderer/components/+workspaces/index.ts b/src/renderer/components/+workspaces/index.ts
index db23faa3be..5b84fc9b00 100644
--- a/src/renderer/components/+workspaces/index.ts
+++ b/src/renderer/components/+workspaces/index.ts
@@ -1,2 +1 @@
-export * from "./workspaces.route";
export * from "./workspaces";
diff --git a/src/renderer/components/+workspaces/remove-workspace.tsx b/src/renderer/components/+workspaces/remove-workspace.tsx
new file mode 100644
index 0000000000..9f66292447
--- /dev/null
+++ b/src/renderer/components/+workspaces/remove-workspace.tsx
@@ -0,0 +1,68 @@
+import React from "react";
+import { observer } from "mobx-react";
+import { computed} from "mobx";
+import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store";
+import { ConfirmDialog } from "../confirm-dialog";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { Select } from "../select";
+import { CommandOverlay } from "../command-palette/command-container";
+
+@observer
+export class RemoveWorkspace extends React.Component {
+ @computed get options() {
+ return workspaceStore.enabledWorkspacesList.filter((workspace) => workspace.id !== WorkspaceStore.defaultId).map((workspace) => {
+ return { value: workspace.id, label: workspace.name };
+ });
+ }
+
+ onChange(id: string) {
+ const workspace = workspaceStore.enabledWorkspacesList.find((workspace) => workspace.id === id);
+
+ if (!workspace ) {
+ return;
+ }
+
+ CommandOverlay.close();
+ ConfirmDialog.open({
+ okButtonProps: {
+ label: `Remove Workspace`,
+ primary: false,
+ accent: true,
+ },
+ ok: () => {
+ workspaceStore.removeWorkspace(workspace);
+ },
+ message: (
+
+
+ Are you sure you want remove workspace {workspace.name} ?
+
+
+ All clusters within workspace will be cleared as well
+
+
+ ),
+ });
+ }
+
+ render() {
+ return (
+ this.onChange(v.value)}
+ components={{ DropdownIndicator: null, IndicatorSeparator: null }}
+ menuIsOpen={true}
+ options={this.options}
+ autoFocus={true}
+ escapeClearsValue={false}
+ data-test-id="command-palette-workspace-remove-select"
+ placeholder="Remove workspace" />
+ );
+ }
+}
+
+commandRegistry.add({
+ id: "workspace.removeWorkspace",
+ title: "Workspace: Remove workspace ...",
+ scope: "global",
+ action: () => CommandOverlay.open( )
+});
diff --git a/src/renderer/components/+workspaces/workspace-menu.scss b/src/renderer/components/+workspaces/workspace-menu.scss
deleted file mode 100644
index e7adf9ae6a..0000000000
--- a/src/renderer/components/+workspaces/workspace-menu.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-.WorkspaceMenu {
- border-radius: $radius;
-
- .workspaces-title {
- padding: $padding;
- }
-}
\ No newline at end of file
diff --git a/src/renderer/components/+workspaces/workspace-menu.tsx b/src/renderer/components/+workspaces/workspace-menu.tsx
deleted file mode 100644
index 30a68cc081..0000000000
--- a/src/renderer/components/+workspaces/workspace-menu.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import "./workspace-menu.scss";
-import React from "react";
-import { observer } from "mobx-react";
-import { Link } from "react-router-dom";
-import { workspacesURL } from "./workspaces.route";
-import { Menu, MenuItem, MenuProps } from "../menu";
-import { Icon } from "../icon";
-import { observable } from "mobx";
-import { WorkspaceId, workspaceStore } from "../../../common/workspace-store";
-import { cssNames } from "../../utils";
-import { navigate } from "../../navigation";
-import { clusterViewURL } from "../cluster-manager/cluster-view.route";
-import { landingURL } from "../+landing-page";
-
-interface Props extends Partial {
-}
-
-@observer
-export class WorkspaceMenu extends React.Component {
- @observable menuVisible = false;
-
- activateWorkspace = (id: WorkspaceId) => {
- const clusterId = workspaceStore.getById(id).lastActiveClusterId;
-
- workspaceStore.setActive(id);
-
- if (clusterId) {
- navigate(clusterViewURL({ params: { clusterId } }));
- } else {
- navigate(landingURL());
- }
- };
-
- render() {
- const { className, ...menuProps } = this.props;
- const { enabledWorkspacesList, currentWorkspace } = workspaceStore;
-
- return (
- this.menuVisible = true}
- close={() => this.menuVisible = false}
- >
-
- Workspaces
-
- {enabledWorkspacesList.map(({ id: workspaceId, name, description }) => {
- return (
- this.activateWorkspace(workspaceId)}
- >
-
- {name}
-
- );
- })}
-
- );
- }
-}
diff --git a/src/renderer/components/+workspaces/workspaces.route.ts b/src/renderer/components/+workspaces/workspaces.route.ts
deleted file mode 100644
index 2429c5315e..0000000000
--- a/src/renderer/components/+workspaces/workspaces.route.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type { RouteProps } from "react-router";
-import { buildURL } from "../../../common/utils/buildUrl";
-
-export const workspacesRoute: RouteProps = {
- path: "/workspaces"
-};
-
-export const workspacesURL = buildURL(workspacesRoute.path);
diff --git a/src/renderer/components/+workspaces/workspaces.scss b/src/renderer/components/+workspaces/workspaces.scss
deleted file mode 100644
index 95c036c304..0000000000
--- a/src/renderer/components/+workspaces/workspaces.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-.Workspaces {
- .workspace {
- --flex-gap: #{$padding};
- padding: $padding / 2;
-
- &.default {
- font-style: italic;
- }
-
- > .description {
- flex: 1;
- }
- }
-}
\ No newline at end of file
diff --git a/src/renderer/components/+workspaces/workspaces.tsx b/src/renderer/components/+workspaces/workspaces.tsx
index 932306adab..7e3b9647f2 100644
--- a/src/renderer/components/+workspaces/workspaces.tsx
+++ b/src/renderer/components/+workspaces/workspaces.tsx
@@ -1,216 +1,89 @@
-import "./workspaces.scss";
-import React, { Fragment } from "react";
+import React from "react";
import { observer } from "mobx-react";
-import { computed, observable, toJS } from "mobx";
-import { WizardLayout } from "../layout/wizard-layout";
-import { Workspace, WorkspaceId, workspaceStore } from "../../../common/workspace-store";
-import { v4 as uuid } from "uuid";
-import { ConfirmDialog } from "../confirm-dialog";
-import { Icon } from "../icon";
-import { Input } from "../input";
-import { cssNames, prevDefault } from "../../utils";
-import { Button } from "../button";
-import { isRequired, InputValidator } from "../input/input_validators";
-import { clusterStore } from "../../../common/cluster-store";
+import { computed} from "mobx";
+import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { Select } from "../select";
+import { navigate } from "../../navigation";
+import { CommandOverlay } from "../command-palette/command-container";
+import { AddWorkspace } from "./add-workspace";
+import { RemoveWorkspace } from "./remove-workspace";
+import { EditWorkspace } from "./edit-workspace";
+import { landingURL } from "../+landing-page";
+import { clusterViewURL } from "../cluster-manager/cluster-view.route";
@observer
-export class Workspaces extends React.Component {
- @observable editingWorkspaces = observable.map();
+export class ChooseWorkspace extends React.Component {
+ private static addActionId = "__add__";
+ private static removeActionId = "__remove__";
+ private static editActionId = "__edit__";
- @computed get workspaces(): Workspace[] {
- const currentWorkspaces: Map = new Map();
-
- workspaceStore.enabledWorkspacesList.forEach((w) => {
- currentWorkspaces.set(w.id, w);
+ @computed get options() {
+ const options = workspaceStore.enabledWorkspacesList.map((workspace) => {
+ return { value: workspace.id, label: workspace.name };
});
- const allWorkspaces = new Map([
- ...currentWorkspaces,
- ...this.editingWorkspaces,
- ]);
- return Array.from(allWorkspaces.values());
+ options.push({ value: ChooseWorkspace.addActionId, label: "Add workspace ..." });
+
+ if (options.length > 1) {
+ options.push({ value: ChooseWorkspace.removeActionId, label: "Remove workspace ..." });
+
+ if (workspaceStore.currentWorkspace.id !== WorkspaceStore.defaultId) {
+ options.push({ value: ChooseWorkspace.editActionId, label: "Edit current workspace ..." });
+ }
+ }
+
+ return options;
}
- renderInfo() {
- return (
-
- What is a Workspace?
-
- Workspaces are used to organize number of clusters into logical groups.
-
-
- A single workspaces contains a list of clusters and their full configuration.
-
-
- );
- }
-
- saveWorkspace = (id: WorkspaceId) => {
- const workspace = new Workspace(this.editingWorkspaces.get(id));
-
- if (workspaceStore.getById(id)) {
- workspaceStore.updateWorkspace(workspace);
- this.clearEditing(id);
+ onChange(id: string) {
+ if (id === ChooseWorkspace.addActionId) {
+ CommandOverlay.open( );
return;
}
- if (workspaceStore.addWorkspace(workspace)) {
- this.clearEditing(id);
+ if (id === ChooseWorkspace.removeActionId) {
+ CommandOverlay.open( );
+
+ return;
}
- };
- addWorkspace = () => {
- const workspaceId = uuid();
+ if (id === ChooseWorkspace.editActionId) {
+ CommandOverlay.open( );
- this.editingWorkspaces.set(workspaceId, new Workspace({
- id: workspaceId,
- name: "",
- description: ""
- }));
- };
-
- editWorkspace = (id: WorkspaceId) => {
- const workspace = workspaceStore.getById(id);
-
- this.editingWorkspaces.set(id, toJS(workspace));
- };
-
- activateWorkspace = (id: WorkspaceId) => {
- const clusterId = workspaceStore.getById(id).lastActiveClusterId;
+ return;
+ }
workspaceStore.setActive(id);
- clusterStore.setActive(clusterId);
- };
+ const clusterId = workspaceStore.getById(id).lastActiveClusterId;
- clearEditing = (id: WorkspaceId) => {
- this.editingWorkspaces.delete(id);
- };
-
- removeWorkspace = (id: WorkspaceId) => {
- const workspace = workspaceStore.getById(id);
-
- ConfirmDialog.open({
- okButtonProps: {
- label: `Remove Workspace`,
- primary: false,
- accent: true,
- },
- ok: () => {
- this.clearEditing(id);
- workspaceStore.removeWorkspace(workspace);
- },
- message: (
-
-
- Are you sure you want remove workspace {workspace.name} ?
-
-
- All clusters within workspace will be cleared as well
-
-
- ),
- });
- };
-
- onInputKeypress = (evt: React.KeyboardEvent, workspaceId: WorkspaceId) => {
- if (evt.key == "Enter") {
- // Trigget input validation
- evt.currentTarget.blur();
- evt.currentTarget.focus();
- this.saveWorkspace(workspaceId);
+ if (clusterId) {
+ navigate(clusterViewURL({ params: { clusterId } }));
+ } else {
+ navigate(landingURL());
}
- };
+
+ CommandOverlay.close();
+ }
render() {
return (
-
-
- Workspaces
-
-
- {this.workspaces.map(({ id: workspaceId, name, description, ownerRef }) => {
- const isActive = workspaceStore.currentWorkspaceId === workspaceId;
- const isDefault = workspaceStore.isDefault(workspaceId);
- const isEditing = this.editingWorkspaces.has(workspaceId);
- const editingWorkspace = this.editingWorkspaces.get(workspaceId);
- const managed = !!ownerRef;
- const className = cssNames("workspace flex gaps align-center", {
- active: isActive,
- editing: isEditing,
- default: isDefault,
- });
- const existenceValidator: InputValidator = {
- message: () => `Workspace '${name}' already exists`,
- validate: value => !workspaceStore.getByName(value.trim())
- };
-
- return (
-
- );
- })}
-
-
-
+ this.onChange(v.value)}
+ components={{ DropdownIndicator: null, IndicatorSeparator: null }}
+ menuIsOpen={true}
+ options={this.options}
+ autoFocus={true}
+ escapeClearsValue={false}
+ placeholder="Switch to workspace" />
);
}
}
+
+commandRegistry.add({
+ id: "workspace.chooseWorkspace",
+ title: "Workspace: Switch to workspace ...",
+ scope: "global",
+ action: () => CommandOverlay.open( )
+});
diff --git a/src/renderer/components/ace-editor/ace-editor.tsx b/src/renderer/components/ace-editor/ace-editor.tsx
index 68ef92635a..2dceb48bba 100644
--- a/src/renderer/components/ace-editor/ace-editor.tsx
+++ b/src/renderer/components/ace-editor/ace-editor.tsx
@@ -154,4 +154,4 @@ export class AceEditor extends React.Component {
);
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/ace-editor/index.ts b/src/renderer/components/ace-editor/index.ts
index 7bfc7c01ea..173845abab 100644
--- a/src/renderer/components/ace-editor/index.ts
+++ b/src/renderer/components/ace-editor/index.ts
@@ -1 +1 @@
-export * from "./ace-editor";
\ No newline at end of file
+export * from "./ace-editor";
diff --git a/src/renderer/components/add-remove-buttons/index.ts b/src/renderer/components/add-remove-buttons/index.ts
index 825c59d7d2..fa2deb84ec 100644
--- a/src/renderer/components/add-remove-buttons/index.ts
+++ b/src/renderer/components/add-remove-buttons/index.ts
@@ -1 +1 @@
-export * from "./add-remove-buttons";
\ No newline at end of file
+export * from "./add-remove-buttons";
diff --git a/src/renderer/components/animate/index.ts b/src/renderer/components/animate/index.ts
index 080c5446c8..36d812de20 100644
--- a/src/renderer/components/animate/index.ts
+++ b/src/renderer/components/animate/index.ts
@@ -1 +1 @@
-export * from "./animate";
\ No newline at end of file
+export * from "./animate";
diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx
index 495182b2a9..8b7f8a527c 100755
--- a/src/renderer/components/app.tsx
+++ b/src/renderer/components/app.tsx
@@ -1,5 +1,6 @@
import React from "react";
-import { observer } from "mobx-react";
+import { computed, observable, reaction } from "mobx";
+import { disposeOnUnmount, observer } from "mobx-react";
import { Redirect, Route, Router, Switch } from "react-router";
import { history } from "../navigation";
import { Notifications } from "./notifications";
@@ -42,11 +43,13 @@ import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../exte
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
import { eventStore } from "./+events/event.store";
-import { computed, reaction } from "mobx";
import { nodesStore } from "./+nodes/nodes.store";
import { podsStore } from "./+workloads-pods/pods.store";
-import { sum } from "lodash";
+import { kubeWatchApi } from "../api/kube-watch-api";
import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale-dialog";
+import { CommandContainer } from "./command-palette/command-container";
+import { KubeObjectStore } from "../kube-object.store";
+import { clusterContext } from "./context";
@observer
export class App extends React.Component {
@@ -73,44 +76,32 @@ export class App extends React.Component {
window.location.reload();
});
whatInput.ask(); // Start to monitor user input device
+
+ // Setup hosted cluster context
+ KubeObjectStore.defaultContext = clusterContext;
+ kubeWatchApi.context = clusterContext;
}
- async componentDidMount() {
- const cluster = getHostedCluster();
- const promises: Promise[] = [];
+ componentDidMount() {
+ disposeOnUnmount(this, [
+ kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore], {
+ preload: true,
+ }),
- if (isAllowedResource("events") && isAllowedResource("pods")) {
- promises.push(eventStore.loadAll());
- promises.push(podsStore.loadAll());
- }
+ reaction(() => this.warningsTotal, (count: number) => {
+ broadcastMessage(`cluster-warning-event-count:${getHostedCluster().id}`, count);
+ }),
- if (isAllowedResource("nodes")) {
- promises.push(nodesStore.loadAll());
- }
- await Promise.all(promises);
-
- if (eventStore.isLoaded && podsStore.isLoaded) {
- eventStore.subscribe();
- podsStore.subscribe();
- }
-
- if (nodesStore.isLoaded) {
- nodesStore.subscribe();
- }
-
- reaction(() => this.warningsCount, (count) => {
- broadcastMessage(`cluster-warning-event-count:${cluster.id}`, count);
- });
+ reaction(() => clusterPageMenuRegistry.getRootItems(), (rootItems) => {
+ this.generateExtensionTabLayoutRoutes(rootItems);
+ }, {
+ fireImmediately: true
+ })
+ ]);
}
- @computed
- get warningsCount() {
- let warnings = sum(nodesStore.items
- .map(node => node.getWarningConditions().length));
-
- warnings = warnings + eventStore.getWarnings().length;
-
- return warnings;
+ @computed get warningsTotal(): number {
+ return nodesStore.getWarningsCount() + eventStore.getWarningsCount();
}
get startURL() {
@@ -161,6 +152,38 @@ export class App extends React.Component {
});
}
+ @observable extensionRoutes: Map = new Map();
+
+ generateExtensionTabLayoutRoutes(rootItems: ClusterPageMenuRegistration[]) {
+ rootItems.forEach((menu, index) => {
+ let route = this.extensionRoutes.get(menu);
+
+ if (!route) {
+ const tabRoutes = this.getTabLayoutRoutes(menu);
+
+ if (tabRoutes.length > 0) {
+ const pageComponent = () => ;
+
+ route = tab.routePath)}/>;
+ this.extensionRoutes.set(menu, route);
+ } else {
+ const page = clusterPageRegistry.getByPageTarget(menu.target);
+
+ if (page) {
+ route = ;
+ this.extensionRoutes.set(menu, route);
+ }
+ }
+ }
+ });
+
+ for (const menu of this.extensionRoutes.keys()) {
+ if (!rootItems.includes(menu)) {
+ this.extensionRoutes.delete(menu);
+ }
+ }
+ }
+
renderExtensionRoutes() {
return clusterPageRegistry.getItems().map((page, index) => {
const menu = clusterPageMenuRegistry.getByPage(page);
@@ -172,6 +195,8 @@ export class App extends React.Component {
}
render() {
+ const cluster = getHostedCluster();
+
return (
@@ -203,6 +228,7 @@ export class App extends React.Component {
+
);
diff --git a/src/renderer/components/button/button.scss b/src/renderer/components/button/button.scss
index b850a3be59..9224b7e5d3 100644
--- a/src/renderer/components/button/button.scss
+++ b/src/renderer/components/button/button.scss
@@ -21,10 +21,16 @@
&.primary {
background: $buttonPrimaryBackground;
}
+
&.accent {
background: $buttonAccentBackground;
}
+ &.light {
+ background-color: $buttonLightBackground;
+ color: #505050;
+ }
+
&.plain {
color: inherit;
background: transparent;
diff --git a/src/renderer/components/button/button.tsx b/src/renderer/components/button/button.tsx
index 9fa822b214..8bcb37bad4 100644
--- a/src/renderer/components/button/button.tsx
+++ b/src/renderer/components/button/button.tsx
@@ -8,6 +8,7 @@ export interface ButtonProps extends ButtonHTMLAttributes, TooltipDecorator
waiting?: boolean;
primary?: boolean;
accent?: boolean;
+ light?: boolean;
plain?: boolean;
outlined?: boolean;
hidden?: boolean;
@@ -24,13 +25,16 @@ export class Button extends React.PureComponent {
private button: HTMLButtonElement;
render() {
- const { className, waiting, label, primary, accent, plain, hidden, active, big, round, outlined, tooltip, children, ...props } = this.props;
- const btnProps = props as Partial;
+ const {
+ className, waiting, label, primary, accent, plain, hidden, active, big,
+ round, outlined, tooltip, light, children, ...props
+ } = this.props;
+ const btnProps: Partial = props;
if (hidden) return null;
btnProps.className = cssNames("Button", className, {
- waiting, primary, accent, plain, active, big, round, outlined
+ waiting, primary, accent, plain, active, big, round, outlined, light,
});
const btnContent: ReactNode = (
diff --git a/src/renderer/components/chart/background-block.plugin.ts b/src/renderer/components/chart/background-block.plugin.ts
index 1d39f71aed..ff4816c4dd 100644
--- a/src/renderer/components/chart/background-block.plugin.ts
+++ b/src/renderer/components/chart/background-block.plugin.ts
@@ -39,4 +39,4 @@ export const BackgroundBlock = {
ctx.stroke();
ctx.restore();
}
-};
\ No newline at end of file
+};
diff --git a/src/renderer/components/chart/bar-chart.tsx b/src/renderer/components/chart/bar-chart.tsx
index 69b65b10c9..74ee203d4c 100644
--- a/src/renderer/components/chart/bar-chart.tsx
+++ b/src/renderer/components/chart/bar-chart.tsx
@@ -3,7 +3,7 @@ import merge from "lodash/merge";
import moment from "moment";
import Color from "color";
import { observer } from "mobx-react";
-import { ChartData, ChartOptions, ChartPoint, Scriptable } from "chart.js";
+import { ChartData, ChartOptions, ChartPoint, ChartTooltipItem, Scriptable } from "chart.js";
import { Chart, ChartKind, ChartProps } from "./chart";
import { bytesToUnits, cssNames } from "../../utils";
import { ZebraStripes } from "./zebra-stripes.plugin";
@@ -50,6 +50,10 @@ export class BarChart extends React.Component {
})
};
+ if (chartData.datasets.length == 0) {
+ return ;
+ }
+
const formatTimeLabels = (timestamp: string, index: number) => {
const label = moment(parseInt(timestamp)).format("HH:mm");
const offset = " ";
@@ -111,12 +115,13 @@ export class BarChart extends React.Component {
mode: "index",
position: "cursor",
callbacks: {
- title: tooltipItems => {
- const now = new Date().getTime();
+ title([tooltip]: ChartTooltipItem[]) {
+ const xLabel = tooltip?.xLabel;
+ const skipLabel = xLabel == null || new Date(xLabel).getTime() > Date.now();
- if (new Date(tooltipItems[0].xLabel).getTime() > now) return "";
+ if (skipLabel) return "";
- return `${tooltipItems[0].xLabel}`;
+ return String(xLabel);
},
labelColor: ({ datasetIndex }) => {
return {
@@ -136,16 +141,13 @@ export class BarChart extends React.Component {
},
plugins: {
ZebraStripes: {
- stripeColor: chartStripesColor
+ stripeColor: chartStripesColor,
+ interval: chartData.datasets[0].data.length
}
}
};
const options = merge(barOptions, customOptions);
- if (chartData.datasets.length == 0) {
- return ;
- }
-
return (
{
>
);
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/chart/index.ts b/src/renderer/components/chart/index.ts
index a9db66c298..d75ddf7a2f 100644
--- a/src/renderer/components/chart/index.ts
+++ b/src/renderer/components/chart/index.ts
@@ -1,3 +1,3 @@
export * from "./chart";
export * from "./pie-chart";
-export * from "./bar-chart";
\ No newline at end of file
+export * from "./bar-chart";
diff --git a/src/renderer/components/chart/pie-chart.tsx b/src/renderer/components/chart/pie-chart.tsx
index 1c629ab505..939d6bb612 100644
--- a/src/renderer/components/chart/pie-chart.tsx
+++ b/src/renderer/components/chart/pie-chart.tsx
@@ -64,4 +64,4 @@ export class PieChart extends React.Component {
ChartJS.Tooltip.positioners.cursor = function (elements: any, position: { x: number; y: number }) {
return position;
-};
\ No newline at end of file
+};
diff --git a/src/renderer/components/chart/useRealTimeMetrics.ts b/src/renderer/components/chart/useRealTimeMetrics.ts
index 69e4e3da7f..b01629e8e9 100644
--- a/src/renderer/components/chart/useRealTimeMetrics.ts
+++ b/src/renderer/components/chart/useRealTimeMetrics.ts
@@ -42,4 +42,4 @@ export function useRealTimeMetrics(metrics: IMetricValues, chartData: IChartData
}
return data;
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/chart/zebra-stripes.plugin.ts b/src/renderer/components/chart/zebra-stripes.plugin.ts
index f934f88fb2..f190401066 100644
--- a/src/renderer/components/chart/zebra-stripes.plugin.ts
+++ b/src/renderer/components/chart/zebra-stripes.plugin.ts
@@ -6,8 +6,6 @@ import moment, { Moment } from "moment";
import get from "lodash/get";
const defaultOptions = {
- interval: 61,
- stripeMinutes: 10,
stripeColor: "#ffffff08",
};
@@ -36,12 +34,23 @@ export const ZebraStripes = {
chart.canvas.parentElement.removeChild(elem);
},
+ updateOptions(chart: ChartJS) {
+ this.options = {
+ ...defaultOptions,
+ ...this.getOptions(chart)
+ };
+ },
+
+ getStripeMinutes() {
+ return this.options.interval < 10 ? 0 : 10;
+ },
+
renderStripes(chart: ChartJS) {
if (!chart.data.datasets.length) return;
- const { interval, stripeMinutes, stripeColor } = this.options;
+ const { interval, stripeColor } = this.options;
const { top, left, bottom, right } = chart.chartArea;
const step = (right - left) / interval;
- const stripeWidth = step * stripeMinutes;
+ const stripeWidth = step * this.getStripeMinutes();
const cover = document.createElement("div");
const styles = cover.style;
@@ -61,14 +70,12 @@ export const ZebraStripes = {
afterInit(chart: ChartJS) {
if (!chart.data.datasets.length) return;
- this.options = {
- ...defaultOptions,
- ...this.getOptions(chart)
- };
+ this.updateOptions(chart);
this.updated = this.getLastUpdate(chart);
},
afterUpdate(chart: ChartJS) {
+ this.updateOptions(chart);
this.renderStripes(chart);
},
@@ -95,4 +102,4 @@ export const ZebraStripes = {
cover.style.backgroundPositionX = `${-step * minutes}px`;
}
}
-};
\ No newline at end of file
+};
diff --git a/src/renderer/components/checkbox/checkbox.tsx b/src/renderer/components/checkbox/checkbox.tsx
index 0831e6122f..8d452a1198 100644
--- a/src/renderer/components/checkbox/checkbox.tsx
+++ b/src/renderer/components/checkbox/checkbox.tsx
@@ -30,7 +30,7 @@ export class Checkbox extends React.PureComponent {
render() {
const { label, inline, className, value, theme, children, ...inputProps } = this.props;
- const componentClass = cssNames("Checkbox flex", className, {
+ const componentClass = cssNames("Checkbox flex align-center", className, {
inline,
checked: value,
disabled: this.props.disabled,
@@ -50,4 +50,4 @@ export class Checkbox extends React.PureComponent {
);
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/checkbox/index.ts b/src/renderer/components/checkbox/index.ts
index 7af8873e06..057f167821 100644
--- a/src/renderer/components/checkbox/index.ts
+++ b/src/renderer/components/checkbox/index.ts
@@ -1 +1 @@
-export * from "./checkbox";
\ No newline at end of file
+export * from "./checkbox";
diff --git a/src/renderer/components/cluster-icon/cluster-icon.tsx b/src/renderer/components/cluster-icon/cluster-icon.tsx
index c1b0ef3577..48d23eb91f 100644
--- a/src/renderer/components/cluster-icon/cluster-icon.tsx
+++ b/src/renderer/components/cluster-icon/cluster-icon.tsx
@@ -52,7 +52,7 @@ export class ClusterIcon extends React.Component {
cluster, showErrors, showTooltip, errorClass, options, interactive, isActive,
children, ...elemProps
} = this.props;
- const { name, preferences, id: clusterId } = cluster;
+ const { name, preferences, id: clusterId, online } = cluster;
const eventCount = this.eventCount;
const { icon } = preferences;
const clusterIconId = `cluster-icon-${clusterId}`;
@@ -68,7 +68,7 @@ export class ClusterIcon extends React.Component {
)}
{icon && }
{!icon && }
- {showErrors && eventCount > 0 && !isActive && (
+ {showErrors && eventCount > 0 && !isActive && online && (
= 1000 ? `${Math.ceil(eventCount / 1000)}k+` : eventCount}
diff --git a/src/renderer/components/cluster-icon/index.ts b/src/renderer/components/cluster-icon/index.ts
index 4e1858939f..7879490b85 100644
--- a/src/renderer/components/cluster-icon/index.ts
+++ b/src/renderer/components/cluster-icon/index.ts
@@ -1 +1 @@
-export * from "./cluster-icon";
\ No newline at end of file
+export * from "./cluster-icon";
diff --git a/src/renderer/components/cluster-manager/bottom-bar.test.tsx b/src/renderer/components/cluster-manager/bottom-bar.test.tsx
index 6578f4ac20..f13121f3a5 100644
--- a/src/renderer/components/cluster-manager/bottom-bar.test.tsx
+++ b/src/renderer/components/cluster-manager/bottom-bar.test.tsx
@@ -14,14 +14,19 @@ describe(" ", () => {
expect(container).toBeInstanceOf(HTMLElement);
});
- // some defensive testing
- it("renders w/o errors when .getItems() returns edge cases", async () => {
+ it("renders w/o errors when .getItems() returns unexpected (not type complient) data", async () => {
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => undefined);
expect(() => render( )).not.toThrow();
+ statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => "hello");
+ expect(() => render( )).not.toThrow();
+ statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => 6);
+ expect(() => render( )).not.toThrow();
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => null);
expect(() => render( )).not.toThrow();
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => []);
expect(() => render( )).not.toThrow();
+ statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => [{}]);
+ expect(() => render( )).not.toThrow();
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => { return {};});
expect(() => render( )).not.toThrow();
});
diff --git a/src/renderer/components/cluster-manager/bottom-bar.tsx b/src/renderer/components/cluster-manager/bottom-bar.tsx
index 489d90769f..e192c67dc6 100644
--- a/src/renderer/components/cluster-manager/bottom-bar.tsx
+++ b/src/renderer/components/cluster-manager/bottom-bar.tsx
@@ -3,40 +3,57 @@ import "./bottom-bar.scss";
import React from "react";
import { observer } from "mobx-react";
import { Icon } from "../icon";
-import { WorkspaceMenu } from "../+workspaces/workspace-menu";
import { workspaceStore } from "../../../common/workspace-store";
-import { statusBarRegistry } from "../../../extensions/registries";
+import { StatusBarRegistration, statusBarRegistry } from "../../../extensions/registries";
+import { CommandOverlay } from "../command-palette/command-container";
+import { ChooseWorkspace } from "../+workspaces";
@observer
export class BottomBar extends React.Component {
+ renderRegisteredItem(registration: StatusBarRegistration) {
+ const { item } = registration;
+
+ if (item) {
+ return typeof item === "function" ? item() : item;
+ }
+
+ return ;
+ }
+
+ renderRegisteredItems() {
+ const items = statusBarRegistry.getItems();
+
+ if (!Array.isArray(items)) {
+ return;
+ }
+
+ return (
+
+ {items.map((registration, index) => {
+ if (!registration?.item && !registration?.components?.Item) {
+ return;
+ }
+
+ return (
+
+ {this.renderRegisteredItem(registration)}
+
+ );
+ })}
+
+ );
+ }
+
render() {
const { currentWorkspace } = workspaceStore;
- // in case .getItems() returns undefined
- const items = statusBarRegistry.getItems() ?? [];
return (
-
+
CommandOverlay.open( )}>
- {currentWorkspace.name}
-
-
-
- {Array.isArray(items) && items.map(({ item }, index) => {
- if (!item) return;
-
- return (
-
- {typeof item === "function" ? item() : item}
-
- );
- })}
+
{currentWorkspace.name}
+ {this.renderRegisteredItems()}
);
}
diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx
index 68a358766c..1c3306b65f 100644
--- a/src/renderer/components/cluster-manager/cluster-manager.tsx
+++ b/src/renderer/components/cluster-manager/cluster-manager.tsx
@@ -8,7 +8,6 @@ import { ClustersMenu } from "./clusters-menu";
import { BottomBar } from "./bottom-bar";
import { LandingPage, landingRoute, landingURL } from "../+landing-page";
import { Preferences, preferencesRoute } from "../+preferences";
-import { Workspaces, workspacesRoute } from "../+workspaces";
import { AddCluster, addClusterRoute } from "../+add-cluster";
import { ClusterView } from "./cluster-view";
import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
@@ -67,7 +66,6 @@ export class ClusterManager extends React.Component {
-
diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx
index 1e79b64f85..d963438136 100644
--- a/src/renderer/components/cluster-manager/clusters-menu.tsx
+++ b/src/renderer/components/cluster-manager/clusters-menu.tsx
@@ -22,6 +22,10 @@ import { ConfirmDialog } from "../confirm-dialog";
import { clusterViewURL } from "./cluster-view.route";
import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
import { clusterDisconnectHandler } from "../../../common/cluster-ipc";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { CommandOverlay } from "../command-palette/command-container";
+import { computed } from "mobx";
+import { Select } from "../select";
interface Props {
className?: IClassName;
@@ -178,3 +182,41 @@ export class ClustersMenu extends React.Component
{
);
}
}
+
+@observer
+export class ChooseCluster extends React.Component {
+ @computed get options() {
+ const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId).filter(cluster => cluster.enabled);
+ const options = clusters.map((cluster) => {
+ return { value: cluster.id, label: cluster.name };
+ });
+
+ return options;
+ }
+
+ onChange(clusterId: string) {
+ navigate(clusterViewURL({ params: { clusterId } }));
+ CommandOverlay.close();
+ }
+
+ render() {
+ return (
+ this.onChange(v.value)}
+ components={{ DropdownIndicator: null, IndicatorSeparator: null }}
+ menuIsOpen={true}
+ options={this.options}
+ autoFocus={true}
+ escapeClearsValue={false}
+ placeholder="Switch to cluster" />
+ );
+ }
+}
+
+
+commandRegistry.add({
+ id: "workspace.chooseCluster",
+ title: "Workspace: Switch to cluster ...",
+ scope: "global",
+ action: () => CommandOverlay.open( )
+});
diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/lens-views.ts
index 17e6c9f00d..a3a92e4f64 100644
--- a/src/renderer/components/cluster-manager/lens-views.ts
+++ b/src/renderer/components/cluster-manager/lens-views.ts
@@ -24,6 +24,7 @@ export async function initView(clusterId: ClusterId) {
if (!cluster) {
return;
}
+
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`);
const parentElem = document.getElementById("lens-views");
const iframe = document.createElement("iframe");
@@ -36,11 +37,21 @@ export async function initView(clusterId: ClusterId) {
}, { once: true });
lensViews.set(clusterId, { clusterId, view: iframe });
parentElem.appendChild(iframe);
+ logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`);
+ await cluster.whenReady;
await autoCleanOnRemove(clusterId, iframe);
}
export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) {
- await when(() => !clusterStore.getById(clusterId));
+ await when(() => {
+ const cluster = clusterStore.getById(clusterId);
+
+ if (!cluster) return true;
+
+ const view = lensViews.get(clusterId);
+
+ return cluster.disconnected && view?.isLoaded;
+ });
logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`);
lensViews.delete(clusterId);
diff --git a/src/renderer/components/command-palette/command-container.scss b/src/renderer/components/command-palette/command-container.scss
new file mode 100644
index 0000000000..c8f76a4698
--- /dev/null
+++ b/src/renderer/components/command-palette/command-container.scss
@@ -0,0 +1,11 @@
+#command-container {
+ position: absolute;
+ top: 20px;
+ width: 40%;
+ left: 0;
+ right: 0;
+ margin-left: auto;
+ margin-right: auto;
+ padding: 10px;
+ background-color: var(--dockInfoBackground);
+}
diff --git a/src/renderer/components/command-palette/command-container.tsx b/src/renderer/components/command-palette/command-container.tsx
new file mode 100644
index 0000000000..34ab71ee67
--- /dev/null
+++ b/src/renderer/components/command-palette/command-container.tsx
@@ -0,0 +1,87 @@
+
+import "./command-container.scss";
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+import React from "react";
+import { Dialog } from "../dialog";
+import { EventEmitter } from "../../../common/event-emitter";
+import { subscribeToBroadcast } from "../../../common/ipc";
+import { CommandDialog } from "./command-dialog";
+import { CommandRegistration, commandRegistry } from "../../../extensions/registries/command-registry";
+import { clusterStore } from "../../../common/cluster-store";
+import { workspaceStore } from "../../../common/workspace-store";
+import { Cluster } from "../../../main/cluster";
+
+export type CommandDialogEvent = {
+ component: React.ReactElement
+};
+
+const commandDialogBus = new EventEmitter<[CommandDialogEvent]>();
+
+export class CommandOverlay {
+ static open(component: React.ReactElement) {
+ commandDialogBus.emit({ component });
+ }
+
+ static close() {
+ commandDialogBus.emit({ component: null });
+ }
+}
+
+@observer
+export class CommandContainer extends React.Component<{cluster?: Cluster}> {
+ @observable.ref commandComponent: React.ReactElement;
+
+ private escHandler(event: KeyboardEvent) {
+ if (event.key === "Escape") {
+ event.stopPropagation();
+ this.closeDialog();
+ }
+ }
+
+ @action
+ private closeDialog() {
+ this.commandComponent = null;
+ }
+
+ private findCommandById(commandId: string) {
+ return commandRegistry.getItems().find((command) => command.id === commandId);
+ }
+
+ private runCommand(command: CommandRegistration) {
+ command.action({
+ cluster: clusterStore.active,
+ workspace: workspaceStore.currentWorkspace
+ });
+ }
+
+ componentDidMount() {
+ if (this.props.cluster) {
+ subscribeToBroadcast(`command-palette:run-action:${this.props.cluster.id}`, (event, commandId: string) => {
+ const command = this.findCommandById(commandId);
+
+ if (command) {
+ this.runCommand(command);
+ }
+ });
+ } else {
+ subscribeToBroadcast("command-palette:open", () => {
+ CommandOverlay.open( );
+ });
+ }
+ window.addEventListener("keyup", (e) => this.escHandler(e), true);
+ commandDialogBus.addListener((event) => {
+ this.commandComponent = event.component;
+ });
+ }
+
+ render() {
+ return (
+ this.commandComponent = null}>
+
+ {this.commandComponent}
+
+
+ );
+ }
+}
diff --git a/src/renderer/components/command-palette/command-dialog.tsx b/src/renderer/components/command-palette/command-dialog.tsx
new file mode 100644
index 0000000000..a8b0965488
--- /dev/null
+++ b/src/renderer/components/command-palette/command-dialog.tsx
@@ -0,0 +1,88 @@
+
+import { Select } from "../select";
+import { computed, observable, toJS } from "mobx";
+import { observer } from "mobx-react";
+import React from "react";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { clusterStore } from "../../../common/cluster-store";
+import { workspaceStore } from "../../../common/workspace-store";
+import { CommandOverlay } from "./command-container";
+import { broadcastMessage } from "../../../common/ipc";
+import { navigate } from "../../navigation";
+import { clusterViewURL } from "../cluster-manager/cluster-view.route";
+
+@observer
+export class CommandDialog extends React.Component {
+ @observable menuIsOpen = true;
+
+ @computed get options() {
+ const context = {
+ cluster: clusterStore.active,
+ workspace: workspaceStore.currentWorkspace
+ };
+
+ return commandRegistry.getItems().filter((command) => {
+ if (command.scope === "cluster" && !clusterStore.active) {
+ return false;
+ }
+
+ if (!command.isActive) {
+ return true;
+ }
+
+ try {
+ return command.isActive(context);
+ } catch(e) {
+ console.error(e);
+
+ return false;
+ }
+ }).map((command) => {
+ return { value: command.id, label: command.title };
+ }).sort((a, b) => a.label > b.label ? 1 : -1);
+ }
+
+ private onChange(value: string) {
+ const command = commandRegistry.getItems().find((cmd) => cmd.id === value);
+
+ if (!command) {
+ return;
+ }
+
+ const action = toJS(command.action);
+
+ try {
+ CommandOverlay.close();
+
+ if (command.scope === "global") {
+ action({
+ cluster: clusterStore.active,
+ workspace: workspaceStore.currentWorkspace
+ });
+ } else if(clusterStore.active) {
+ navigate(clusterViewURL({
+ params: {
+ clusterId: clusterStore.active.id
+ }
+ }));
+ broadcastMessage(`command-palette:run-action:${clusterStore.active.id}`, command.id);
+ }
+ } catch(error) {
+ console.error("[COMMAND-DIALOG] failed to execute command", command.id, error);
+ }
+ }
+
+ render() {
+ return (
+ this.onChange(v.value)}
+ components={{ DropdownIndicator: null, IndicatorSeparator: null }}
+ menuIsOpen={this.menuIsOpen}
+ options={this.options}
+ autoFocus={true}
+ escapeClearsValue={false}
+ data-test-id="command-palette-search"
+ placeholder="" />
+ );
+ }
+}
diff --git a/src/renderer/components/command-palette/index.ts b/src/renderer/components/command-palette/index.ts
new file mode 100644
index 0000000000..c01da0f7b6
--- /dev/null
+++ b/src/renderer/components/command-palette/index.ts
@@ -0,0 +1,2 @@
+export * from "./command-container";
+export * from "./command-dialog";
diff --git a/src/renderer/components/confirm-dialog/index.ts b/src/renderer/components/confirm-dialog/index.ts
index dfcd83ded3..4627fd6882 100644
--- a/src/renderer/components/confirm-dialog/index.ts
+++ b/src/renderer/components/confirm-dialog/index.ts
@@ -1 +1 @@
-export * from "./confirm-dialog";
\ No newline at end of file
+export * from "./confirm-dialog";
diff --git a/src/renderer/components/context.ts b/src/renderer/components/context.ts
new file mode 100755
index 0000000000..e8c9b1327d
--- /dev/null
+++ b/src/renderer/components/context.ts
@@ -0,0 +1,38 @@
+import type { Cluster } from "../../main/cluster";
+import { getHostedCluster } from "../../common/cluster-store";
+import { namespaceStore } from "./+namespaces/namespace.store";
+
+export interface ClusterContext {
+ cluster?: Cluster;
+ allNamespaces?: string[]; // available / allowed namespaces from cluster.ts
+ contextNamespaces?: string[]; // selected by user (see: namespace-select.tsx)
+}
+
+export const clusterContext: ClusterContext = {
+ get cluster(): Cluster | null {
+ return getHostedCluster();
+ },
+
+ get allNamespaces(): string[] {
+ if (!this.cluster) {
+ return [];
+ }
+
+ // user given list of namespaces
+ if (this.cluster?.accessibleNamespaces.length) {
+ return this.cluster.accessibleNamespaces;
+ }
+
+ if (namespaceStore.items.length > 0) {
+ // namespaces from kubernetes api
+ return namespaceStore.items.map((namespace) => namespace.getName());
+ } else {
+ // fallback to cluster resolved namespaces because we could not load list
+ return this.cluster.allowedNamespaces || [];
+ }
+ },
+
+ get contextNamespaces(): string[] {
+ return namespaceStore.contextNamespaces ?? [];
+ },
+};
diff --git a/src/renderer/components/dialog/dialog.tsx b/src/renderer/components/dialog/dialog.tsx
index 232fee6475..81851616e8 100644
--- a/src/renderer/components/dialog/dialog.tsx
+++ b/src/renderer/components/dialog/dialog.tsx
@@ -146,11 +146,10 @@ export class Dialog extends React.PureComponent {
{dialog}
);
- }
- else if (!this.isOpen) {
+ } else if (!this.isOpen) {
return null;
}
- return createPortal(dialog, document.body);
+ return createPortal(dialog, document.body) as React.ReactPortal;
}
}
diff --git a/src/renderer/components/dock/__test__/dock-tabs.test.tsx b/src/renderer/components/dock/__test__/dock-tabs.test.tsx
index bcf6b94a2b..f893e06540 100644
--- a/src/renderer/components/dock/__test__/dock-tabs.test.tsx
+++ b/src/renderer/components/dock/__test__/dock-tabs.test.tsx
@@ -4,8 +4,6 @@ import "@testing-library/jest-dom/extend-expect";
import { DockTabs } from "../dock-tabs";
import { dockStore, IDockTab, TabKind } from "../dock.store";
-import { createResourceTab } from "../create-resource.store";
-import { createTerminalTab } from "../terminal.store";
import { observable } from "mobx";
const onChangeTab = jest.fn();
@@ -25,11 +23,19 @@ const getTabKinds = () => dockStore.tabs.map(tab => tab.kind);
describe(" ", () => {
beforeEach(() => {
- createTerminalTab();
- createResourceTab();
- createTerminalTab();
- createResourceTab();
- createTerminalTab();
+ const terminalTab: IDockTab = { id: "terminal1", kind: TabKind.TERMINAL, title: "Terminal" };
+ const createResourceTab: IDockTab = { id: "create", kind: TabKind.CREATE_RESOURCE, title: "Create resource" };
+ const editResourceTab: IDockTab = { id: "edit", kind: TabKind.EDIT_RESOURCE, title: "Edit resource" };
+ const installChartTab: IDockTab = { id: "install", kind: TabKind.INSTALL_CHART, title: "Install chart" };
+ const logsTab: IDockTab = { id: "logs", kind: TabKind.POD_LOGS, title: "Logs" };
+
+ dockStore.tabs.push(
+ terminalTab,
+ createResourceTab,
+ editResourceTab,
+ installChartTab,
+ logsTab
+ );
});
afterEach(() => {
@@ -72,9 +78,9 @@ describe(" ", () => {
expect(getTabKinds()).toEqual([
TabKind.TERMINAL,
TabKind.CREATE_RESOURCE,
- TabKind.TERMINAL,
- TabKind.CREATE_RESOURCE,
- TabKind.TERMINAL
+ TabKind.EDIT_RESOURCE,
+ TabKind.INSTALL_CHART,
+ TabKind.POD_LOGS
]);
});
@@ -90,7 +96,7 @@ describe(" ", () => {
const tabs = container.querySelectorAll(".Tab");
expect(tabs.length).toBe(1);
- expect(getTabKinds()).toEqual([TabKind.TERMINAL]);
+ expect(getTabKinds()).toEqual([TabKind.EDIT_RESOURCE]);
});
it("closes all tabs", () => {
@@ -123,7 +129,7 @@ describe(" ", () => {
TabKind.TERMINAL,
TabKind.TERMINAL,
TabKind.CREATE_RESOURCE,
- TabKind.TERMINAL
+ TabKind.EDIT_RESOURCE
]);
});
diff --git a/src/renderer/components/dock/__test__/log-resource-selector.test.tsx b/src/renderer/components/dock/__test__/log-resource-selector.test.tsx
new file mode 100644
index 0000000000..22d97b7216
--- /dev/null
+++ b/src/renderer/components/dock/__test__/log-resource-selector.test.tsx
@@ -0,0 +1,103 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import React from "react";
+import "@testing-library/jest-dom/extend-expect";
+import { render } from "@testing-library/react";
+import selectEvent from "react-select-event";
+
+import { Pod } from "../../../api/endpoints";
+import { LogResourceSelector } from "../log-resource-selector";
+import { LogTabData } from "../log-tab.store";
+import { dockerPod, deploymentPod1 } from "./pod.mock";
+
+const getComponent = (tabData: LogTabData) => {
+ return (
+
+ );
+};
+
+const getOnePodTabData = (): LogTabData => {
+ const selectedPod = new Pod(dockerPod);
+
+ return {
+ pods: [] as Pod[],
+ selectedPod,
+ selectedContainer: selectedPod.getContainers()[0],
+ };
+};
+
+const getFewPodsTabData = (): LogTabData => {
+ const selectedPod = new Pod(deploymentPod1);
+ const anotherPod = new Pod(dockerPod);
+
+ return {
+ pods: [anotherPod],
+ selectedPod,
+ selectedContainer: selectedPod.getContainers()[0],
+ };
+};
+
+describe(" ", () => {
+ it("renders w/o errors", () => {
+ const tabData = getOnePodTabData();
+ const { container } = render(getComponent(tabData));
+
+ expect(container).toBeInstanceOf(HTMLElement);
+ });
+
+ it("renders proper namespace", () => {
+ const tabData = getOnePodTabData();
+ const { getByTestId } = render(getComponent(tabData));
+ const ns = getByTestId("namespace-badge");
+
+ expect(ns).toHaveTextContent("default");
+ });
+
+ it("renders proper selected items within dropdowns", () => {
+ const tabData = getOnePodTabData();
+ const { getByText } = render(getComponent(tabData));
+
+ expect(getByText("dockerExporter")).toBeInTheDocument();
+ expect(getByText("docker-exporter")).toBeInTheDocument();
+ });
+
+ it("renders sibling pods in dropdown", () => {
+ const tabData = getFewPodsTabData();
+ const { container, getByText } = render(getComponent(tabData));
+ const podSelector: HTMLElement = container.querySelector(".pod-selector");
+
+ selectEvent.openMenu(podSelector);
+
+ expect(getByText("dockerExporter")).toBeInTheDocument();
+ expect(getByText("deploymentPod1")).toBeInTheDocument();
+ });
+
+ it("renders sibling containers in dropdown", () => {
+ const tabData = getFewPodsTabData();
+ const { getByText, container } = render(getComponent(tabData));
+ const containerSelector: HTMLElement = container.querySelector(".container-selector");
+
+ selectEvent.openMenu(containerSelector);
+
+ expect(getByText("node-exporter-1")).toBeInTheDocument();
+ expect(getByText("init-node-exporter")).toBeInTheDocument();
+ expect(getByText("init-node-exporter-1")).toBeInTheDocument();
+ });
+
+ it("renders pod owner as dropdown title", () => {
+ const tabData = getFewPodsTabData();
+ const { getByText, container } = render(getComponent(tabData));
+ const podSelector: HTMLElement = container.querySelector(".pod-selector");
+
+ selectEvent.openMenu(podSelector);
+
+ expect(getByText("super-deployment")).toBeInTheDocument();
+ });
+});
diff --git a/src/renderer/components/dock/__test__/log-tab.store.test.ts b/src/renderer/components/dock/__test__/log-tab.store.test.ts
new file mode 100644
index 0000000000..79b93af623
--- /dev/null
+++ b/src/renderer/components/dock/__test__/log-tab.store.test.ts
@@ -0,0 +1,113 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import { podsStore } from "../../+workloads-pods/pods.store";
+import { Pod } from "../../../api/endpoints";
+import { dockStore } from "../dock.store";
+import { logTabStore } from "../log-tab.store";
+import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
+
+
+podsStore.items.push(new Pod(dockerPod));
+podsStore.items.push(new Pod(deploymentPod1));
+podsStore.items.push(new Pod(deploymentPod2));
+
+describe("log tab store", () => {
+ afterEach(() => {
+ logTabStore.reset();
+ dockStore.reset();
+ });
+
+ it("creates log tab without sibling pods", () => {
+ const selectedPod = new Pod(dockerPod);
+ const selectedContainer = selectedPod.getAllContainers()[0];
+
+ logTabStore.createPodTab({
+ selectedPod,
+ selectedContainer
+ });
+
+ expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
+ pods: [selectedPod],
+ selectedPod,
+ selectedContainer,
+ showTimestamps: false,
+ previous: false
+ });
+ });
+
+ it("creates log tab with sibling pods", () => {
+ const selectedPod = new Pod(deploymentPod1);
+ const siblingPod = new Pod(deploymentPod2);
+ const selectedContainer = selectedPod.getInitContainers()[0];
+
+ logTabStore.createPodTab({
+ selectedPod,
+ selectedContainer
+ });
+
+ expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
+ pods: [selectedPod, siblingPod],
+ selectedPod,
+ selectedContainer,
+ showTimestamps: false,
+ previous: false
+ });
+ });
+
+ it("removes item from pods list if pod deleted from store", () => {
+ const selectedPod = new Pod(deploymentPod1);
+ const selectedContainer = selectedPod.getInitContainers()[0];
+
+ logTabStore.createPodTab({
+ selectedPod,
+ selectedContainer
+ });
+
+ podsStore.items.pop();
+
+ expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
+ pods: [selectedPod],
+ selectedPod,
+ selectedContainer,
+ showTimestamps: false,
+ previous: false
+ });
+ });
+
+ it("adds item into pods list if new sibling pod added to store", () => {
+ const selectedPod = new Pod(deploymentPod1);
+ const selectedContainer = selectedPod.getInitContainers()[0];
+
+ logTabStore.createPodTab({
+ selectedPod,
+ selectedContainer
+ });
+
+ podsStore.items.push(new Pod(deploymentPod3));
+
+ expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
+ pods: [selectedPod, deploymentPod3],
+ selectedPod,
+ selectedContainer,
+ showTimestamps: false,
+ previous: false
+ });
+ });
+
+ it("closes tab if no pods left in store", () => {
+ const selectedPod = new Pod(deploymentPod1);
+ const selectedContainer = selectedPod.getInitContainers()[0];
+
+ logTabStore.createPodTab({
+ selectedPod,
+ selectedContainer
+ });
+
+ podsStore.items.clear();
+
+ expect(logTabStore.getData(dockStore.selectedTabId)).toBeUndefined();
+ expect(dockStore.getTabById(dockStore.selectedTabId)).toBeUndefined();
+ });
+});
diff --git a/src/renderer/components/dock/__test__/pod.mock.ts b/src/renderer/components/dock/__test__/pod.mock.ts
new file mode 100644
index 0000000000..acb4704395
--- /dev/null
+++ b/src/renderer/components/dock/__test__/pod.mock.ts
@@ -0,0 +1,203 @@
+export const dockerPod = {
+ apiVersion: "v1",
+ kind: "dummy",
+ metadata: {
+ uid: "dockerExporter",
+ name: "dockerExporter",
+ creationTimestamp: "dummy",
+ resourceVersion: "dummy",
+ namespace: "default"
+ },
+ spec: {
+ initContainers: [] as any,
+ containers: [
+ {
+ name: "docker-exporter",
+ image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
+ imagePullPolicy: "pull"
+ }
+ ],
+ serviceAccountName: "dummy",
+ serviceAccount: "dummy",
+ },
+ status: {
+ phase: "Running",
+ conditions: [{
+ type: "Running",
+ status: "Running",
+ lastProbeTime: 1,
+ lastTransitionTime: "Some time",
+ }],
+ hostIP: "dummy",
+ podIP: "dummy",
+ startTime: "dummy",
+ }
+};
+
+export const deploymentPod1 = {
+ apiVersion: "v1",
+ kind: "dummy",
+ metadata: {
+ uid: "deploymentPod1",
+ name: "deploymentPod1",
+ creationTimestamp: "dummy",
+ resourceVersion: "dummy",
+ namespace: "default",
+ ownerReferences: [{
+ apiVersion: "v1",
+ kind: "Deployment",
+ name: "super-deployment",
+ uid: "uuid",
+ controller: true,
+ blockOwnerDeletion: true,
+ }]
+ },
+ spec: {
+ initContainers: [
+ {
+ name: "init-node-exporter",
+ image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
+ imagePullPolicy: "pull"
+ },
+ {
+ name: "init-node-exporter-1",
+ image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
+ imagePullPolicy: "pull"
+ }
+ ],
+ containers: [
+ {
+ name: "node-exporter",
+ image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
+ imagePullPolicy: "pull"
+ },
+ {
+ name: "node-exporter-1",
+ image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
+ imagePullPolicy: "pull"
+ }
+ ],
+ serviceAccountName: "dummy",
+ serviceAccount: "dummy",
+ },
+ status: {
+ phase: "Running",
+ conditions: [{
+ type: "Running",
+ status: "Running",
+ lastProbeTime: 1,
+ lastTransitionTime: "Some time",
+ }],
+ hostIP: "dummy",
+ podIP: "dummy",
+ startTime: "dummy",
+ }
+};
+
+export const deploymentPod2 = {
+ apiVersion: "v1",
+ kind: "dummy",
+ metadata: {
+ uid: "deploymentPod2",
+ name: "deploymentPod2",
+ creationTimestamp: "dummy",
+ resourceVersion: "dummy",
+ namespace: "default",
+ ownerReferences: [{
+ apiVersion: "v1",
+ kind: "Deployment",
+ name: "super-deployment",
+ uid: "uuid",
+ controller: true,
+ blockOwnerDeletion: true,
+ }]
+ },
+ spec: {
+ initContainers: [
+ {
+ name: "init-node-exporter",
+ image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
+ imagePullPolicy: "pull"
+ },
+ {
+ name: "init-node-exporter-1",
+ image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
+ imagePullPolicy: "pull"
+ }
+ ],
+ containers: [
+ {
+ name: "node-exporter",
+ image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
+ imagePullPolicy: "pull"
+ },
+ {
+ name: "node-exporter-1",
+ image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
+ imagePullPolicy: "pull"
+ }
+ ],
+ serviceAccountName: "dummy",
+ serviceAccount: "dummy",
+ },
+ status: {
+ phase: "Running",
+ conditions: [{
+ type: "Running",
+ status: "Running",
+ lastProbeTime: 1,
+ lastTransitionTime: "Some time",
+ }],
+ hostIP: "dummy",
+ podIP: "dummy",
+ startTime: "dummy",
+ }
+};
+
+export const deploymentPod3 = {
+ apiVersion: "v1",
+ kind: "dummy",
+ metadata: {
+ uid: "deploymentPod3",
+ name: "deploymentPod3",
+ creationTimestamp: "dummy",
+ resourceVersion: "dummy",
+ namespace: "default",
+ ownerReferences: [{
+ apiVersion: "v1",
+ kind: "Deployment",
+ name: "super-deployment",
+ uid: "uuid",
+ controller: true,
+ blockOwnerDeletion: true,
+ }]
+ },
+ spec: {
+ containers: [
+ {
+ name: "node-exporter",
+ image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
+ imagePullPolicy: "pull"
+ },
+ {
+ name: "node-exporter-1",
+ image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
+ imagePullPolicy: "pull"
+ }
+ ],
+ serviceAccountName: "dummy",
+ serviceAccount: "dummy",
+ },
+ status: {
+ phase: "Running",
+ conditions: [{
+ type: "Running",
+ status: "Running",
+ lastProbeTime: 1,
+ lastTransitionTime: "Some time",
+ }],
+ hostIP: "dummy",
+ podIP: "dummy",
+ startTime: "dummy",
+ }
+};
diff --git a/src/renderer/components/dock/create-resource.tsx b/src/renderer/components/dock/create-resource.tsx
index 100ef6a3ae..8ee859d2cf 100644
--- a/src/renderer/components/dock/create-resource.tsx
+++ b/src/renderer/components/dock/create-resource.tsx
@@ -77,7 +77,7 @@ export class CreateResource extends React.Component {
tabId={tabId}
error={error}
submit={create}
- submitLabel={`Create`}
+ submitLabel="Create"
showNotifications={false}
/>
{
{!pinned && (
)}
diff --git a/src/renderer/components/dock/dock-tabs.tsx b/src/renderer/components/dock/dock-tabs.tsx
index 54451ddd89..d0a3c3d125 100644
--- a/src/renderer/components/dock/dock-tabs.tsx
+++ b/src/renderer/components/dock/dock-tabs.tsx
@@ -7,7 +7,7 @@ import { DockTab } from "./dock-tab";
import { IDockTab } from "./dock.store";
import { isEditResourceTab } from "./edit-resource.store";
import { isInstallChartTab } from "./install-chart.store";
-import { isPodLogsTab } from "./pod-logs.store";
+import { isLogsTab } from "./log-tab.store";
import { TerminalTab } from "./terminal-tab";
import { isTerminalTab } from "./terminal.store";
import { isUpgradeChartTab } from "./upgrade-chart.store";
@@ -33,7 +33,7 @@ export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: Props) =
return } />;
}
- if (isPodLogsTab(tab)) {
+ if (isLogsTab(tab)) {
return ;
}
};
@@ -48,4 +48,4 @@ export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: Props) =
{tabs.map(tab => {renderTab(tab)} )}
);
-};
\ No newline at end of file
+};
diff --git a/src/renderer/components/dock/dock.store.ts b/src/renderer/components/dock/dock.store.ts
index 91d72d98d9..423367093e 100644
--- a/src/renderer/components/dock/dock.store.ts
+++ b/src/renderer/components/dock/dock.store.ts
@@ -208,6 +208,12 @@ export class DockStore {
this.closeTabs(tabs);
}
+ renameTab(tabId: TabId, title: string) {
+ const tab = this.getTabById(tabId);
+
+ tab.title = title;
+ }
+
@action
selectTab(tabId: TabId) {
this.selectedTabId = this.getTabById(tabId)?.id ?? null;
diff --git a/src/renderer/components/dock/dock.tsx b/src/renderer/components/dock/dock.tsx
index f02502eab7..6d74544f45 100644
--- a/src/renderer/components/dock/dock.tsx
+++ b/src/renderer/components/dock/dock.tsx
@@ -16,12 +16,13 @@ import { EditResource } from "./edit-resource";
import { isEditResourceTab } from "./edit-resource.store";
import { InstallChart } from "./install-chart";
import { isInstallChartTab } from "./install-chart.store";
-import { PodLogs } from "./pod-logs";
-import { isPodLogsTab } from "./pod-logs.store";
+import { Logs } from "./logs";
+import { isLogsTab } from "./log-tab.store";
import { TerminalWindow } from "./terminal-window";
import { createTerminalTab, isTerminalTab } from "./terminal.store";
import { UpgradeChart } from "./upgrade-chart";
import { isUpgradeChartTab } from "./upgrade-chart.store";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
interface Props {
className?: string;
@@ -64,7 +65,7 @@ export class Dock extends React.Component {
{isInstallChartTab(tab) && }
{isUpgradeChartTab(tab) && }
{isTerminalTab(tab) && }
- {isPodLogsTab(tab) && }
+ {isLogsTab(tab) && }
);
}
@@ -131,3 +132,11 @@ export class Dock extends React.Component {
);
}
}
+
+commandRegistry.add({
+ id: "cluster.openTerminal",
+ title: "Cluster: Open terminal",
+ scope: "cluster",
+ action: () => createTerminalTab(),
+ isActive: (context) => !!context.cluster
+});
diff --git a/src/renderer/components/dock/edit-resource.tsx b/src/renderer/components/dock/edit-resource.tsx
index e33e379620..104f8f1ea3 100644
--- a/src/renderer/components/dock/edit-resource.tsx
+++ b/src/renderer/components/dock/edit-resource.tsx
@@ -98,8 +98,8 @@ export class EditResource extends React.Component {
tabId={tabId}
error={error}
submit={save}
- submitLabel={`Save`}
- submittingMessage={`Applying..`}
+ submitLabel="Save"
+ submittingMessage="Applying.."
controls={(
Kind:
diff --git a/src/renderer/components/dock/info-panel.scss b/src/renderer/components/dock/info-panel.scss
index 23dcc52243..cf9b3268b2 100644
--- a/src/renderer/components/dock/info-panel.scss
+++ b/src/renderer/components/dock/info-panel.scss
@@ -1,13 +1,16 @@
.InfoPanel {
@include hidden-scrollbar;
- background: $dockInfoBackground;
- border-bottom: 1px solid $dockInfoBorderColor;
- padding: $padding $padding * 2;
+ background: var(--dockInfoBackground);
+ padding: var(--padding) calc(var(--padding) * 2);
flex-shrink: 0;
.Spinner {
- margin-right: $padding;
+ margin-right: var(--padding);
+ }
+
+ .Badge {
+ background-color: var(--dockBadgeBackground);
}
> .controls {
@@ -16,8 +19,8 @@
&:not(:empty) + .info {
min-height: 25px;
- padding-left: $padding;
- padding-right: $padding;
+ padding-left: var(--padding);
+ padding-right: var(--padding);
}
}
}
\ No newline at end of file
diff --git a/src/renderer/components/dock/install-chart.tsx b/src/renderer/components/dock/install-chart.tsx
index b433874a75..4f651f7a3a 100644
--- a/src/renderer/components/dock/install-chart.tsx
+++ b/src/renderer/components/dock/install-chart.tsx
@@ -125,17 +125,17 @@ export class InstallChart extends Component {
this.showNotes = true}
/>
this.showNotes = false}
logs={this.releaseDetails.log}
@@ -148,7 +148,7 @@ export class InstallChart extends Component {
const panelControls = (
Chart
-
+
Version
{
onChange={this.onNamespaceChange}
/>
{
controls={panelControls}
error={this.error}
submit={install}
- submitLabel={`Install`}
- submittingMessage={`Installing...`}
+ submitLabel="Install"
+ submittingMessage="Installing..."
showSubmitClose={false}
/>
) => void
+ reload: () => void
+}
+
+export const LogControls = observer((props: Props) => {
+ const { tabData, save, reload, logs } = props;
+ const { showTimestamps, previous } = tabData;
+ const since = logs.length ? logStore.getTimestamps(logs[0]) : null;
+ const pod = new Pod(tabData.selectedPod);
+
+ const toggleTimestamps = () => {
+ save({ showTimestamps: !showTimestamps });
+ };
+
+ const togglePrevious = () => {
+ save({ previous: !previous });
+ reload();
+ };
+
+ const downloadLogs = () => {
+ const fileName = pod.getName();
+ const logsToDownload = showTimestamps ? logs : logStore.logsWithoutTimestamps;
+
+ saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain");
+ };
+
+ return (
+
+
+ {since && (
+
+ Logs from{" "}
+ {new Date(since[0]).toLocaleString()}
+
+ )}
+
+
+
+
+
+
+
+ );
+});
diff --git a/src/renderer/components/dock/pod-log-list.scss b/src/renderer/components/dock/log-list.scss
similarity index 99%
rename from src/renderer/components/dock/pod-log-list.scss
rename to src/renderer/components/dock/log-list.scss
index 9b923b520b..8a39dcf925 100644
--- a/src/renderer/components/dock/pod-log-list.scss
+++ b/src/renderer/components/dock/log-list.scss
@@ -1,4 +1,4 @@
-.PodLogList {
+.LogList {
--overlay-bg: #8cc474b8;
--overlay-active-bg: orange;
diff --git a/src/renderer/components/dock/pod-log-list.tsx b/src/renderer/components/dock/log-list.tsx
similarity index 92%
rename from src/renderer/components/dock/pod-log-list.tsx
rename to src/renderer/components/dock/log-list.tsx
index c876d0c362..3b66f42d86 100644
--- a/src/renderer/components/dock/pod-log-list.tsx
+++ b/src/renderer/components/dock/log-list.tsx
@@ -1,4 +1,4 @@
-import "./pod-log-list.scss";
+import "./log-list.scss";
import React from "react";
import AnsiUp from "ansi_up";
@@ -14,7 +14,8 @@ import { Button } from "../button";
import { Icon } from "../icon";
import { Spinner } from "../spinner";
import { VirtualList } from "../virtual-list";
-import { podLogsStore } from "./pod-logs.store";
+import { logStore } from "./log.store";
+import { logTabStore } from "./log-tab.store";
interface Props {
logs: string[]
@@ -26,7 +27,7 @@ interface Props {
const colorConverter = new AnsiUp();
@observer
-export class PodLogList extends React.Component {
+export class LogList extends React.Component {
@observable isJumpButtonVisible = false;
@observable isLastLineVisible = true;
@@ -77,10 +78,10 @@ export class PodLogList extends React.Component {
*/
@computed
get logs() {
- const showTimestamps = podLogsStore.getData(this.props.id).showTimestamps;
+ const showTimestamps = logTabStore.getData(this.props.id).showTimestamps;
if (!showTimestamps) {
- return podLogsStore.logsWithoutTimestamps;
+ return logStore.logsWithoutTimestamps;
}
return this.props.logs;
@@ -206,19 +207,23 @@ export class PodLogList extends React.Component {
const rowHeights = new Array(this.logs.length).fill(this.lineHeight);
if (isInitLoading) {
- return ;
+ return (
+
+
+
+ );
}
if (!this.logs.length) {
return (
-
+
There are no logs available for container
);
}
return (
-
+
) => void
+ reload: () => void
+}
+
+export const LogResourceSelector = observer((props: Props) => {
+ const { tabData, save, reload, tabId } = props;
+ const { selectedPod, selectedContainer, pods } = tabData;
+ const pod = new Pod(selectedPod);
+ const containers = pod.getContainers();
+ const initContainers = pod.getInitContainers();
+
+ const onContainerChange = (option: SelectOption) => {
+ save({
+ selectedContainer: containers
+ .concat(initContainers)
+ .find(container => container.name === option.value)
+ });
+ reload();
+ };
+
+ const onPodChange = (option: SelectOption) => {
+ const selectedPod = podsStore.getByName(option.value, pod.getNs());
+
+ save({ selectedPod });
+ logTabStore.renameTab(tabId);
+ };
+
+ const getSelectOptions = (items: string[]) => {
+ return items.map(item => {
+ return {
+ value: item,
+ label: item
+ };
+ });
+ };
+
+ const containerSelectOptions = [
+ {
+ label: `Containers`,
+ options: getSelectOptions(containers.map(container => container.name))
+ },
+ {
+ label: `Init Containers`,
+ options: getSelectOptions(initContainers.map(container => container.name)),
+ }
+ ];
+
+ const podSelectOptions = [
+ {
+ label: pod.getOwnerRefs()[0]?.name,
+ options: getSelectOptions(pods.map(pod => pod.metadata.name))
+ }
+ ];
+
+ useEffect(() => {
+ reload();
+ }, [selectedPod]);
+
+ return (
+
+ Namespace
+ Pod
+
+ Container
+
+
+ );
+});
diff --git a/src/renderer/components/dock/pod-log-search.scss b/src/renderer/components/dock/log-search.scss
similarity index 61%
rename from src/renderer/components/dock/pod-log-search.scss
rename to src/renderer/components/dock/log-search.scss
index 7d3ea9d92a..eec9a74749 100644
--- a/src/renderer/components/dock/pod-log-search.scss
+++ b/src/renderer/components/dock/log-search.scss
@@ -1,10 +1,13 @@
-.PodLogsSearch {
+.LogSearch {
.SearchInput {
min-width: 150px;
- width: 150px;
.find-count {
margin-left: 2px;
}
+
+ label {
+ padding-bottom: 7px;
+ }
}
}
\ No newline at end of file
diff --git a/src/renderer/components/dock/pod-log-search.tsx b/src/renderer/components/dock/log-search.tsx
similarity index 83%
rename from src/renderer/components/dock/pod-log-search.tsx
rename to src/renderer/components/dock/log-search.tsx
index 675c556c3e..c2bccec59b 100644
--- a/src/renderer/components/dock/pod-log-search.tsx
+++ b/src/renderer/components/dock/log-search.tsx
@@ -1,4 +1,4 @@
-import "./pod-log-search.scss";
+import "./log-search.scss";
import React, { useEffect } from "react";
import { observer } from "mobx-react";
@@ -16,7 +16,7 @@ interface Props extends PodLogSearchProps {
logs: string[]
}
-export const PodLogSearch = observer((props: Props) => {
+export const LogSearch = observer((props: Props) => {
const { logs, onSearch, toPrevOverlay, toNextOverlay } = props;
const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = searchStore;
const jumpDisabled = !searchQuery || !occurrences.length;
@@ -57,32 +57,27 @@ export const PodLogSearch = observer((props: Props) => {
}, [logs]);
return (
-
+
0 && findCounts}
onClear={onClear}
onKeyDown={onKeyDown}
/>
-
);
});
diff --git a/src/renderer/components/dock/log-tab.store.ts b/src/renderer/components/dock/log-tab.store.ts
new file mode 100644
index 0000000000..3eec7812be
--- /dev/null
+++ b/src/renderer/components/dock/log-tab.store.ts
@@ -0,0 +1,123 @@
+import uniqueId from "lodash/uniqueId";
+import { reaction } from "mobx";
+import { podsStore } from "../+workloads-pods/pods.store";
+
+import { IPodContainer, Pod } from "../../api/endpoints";
+import { WorkloadKubeObject } from "../../api/workload-kube-object";
+import { DockTabStore } from "./dock-tab.store";
+import { dockStore, IDockTab, TabKind } from "./dock.store";
+
+export interface LogTabData {
+ pods: Pod[];
+ selectedPod: Pod;
+ selectedContainer: IPodContainer
+ showTimestamps?: boolean
+ previous?: boolean
+}
+
+interface PodLogsTabData {
+ selectedPod: Pod
+ selectedContainer: IPodContainer
+}
+
+interface WorkloadLogsTabData {
+ workload: WorkloadKubeObject
+}
+
+export class LogTabStore extends DockTabStore
{
+ constructor() {
+ super({
+ storageName: "pod_logs"
+ });
+
+ reaction(() => podsStore.items.length, () => {
+ this.updateTabsData();
+ });
+ }
+
+ createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): void {
+ const podOwner = selectedPod.getOwnerRefs()[0];
+ const pods = podsStore.getPodsByOwnerId(podOwner?.uid);
+ const title = `Pod ${selectedPod.getName()}`;
+
+ this.createLogsTab(title, {
+ pods: pods.length ? pods : [selectedPod],
+ selectedPod,
+ selectedContainer
+ });
+ }
+
+ createWorkloadTab({ workload }: WorkloadLogsTabData): void {
+ const pods = podsStore.getPodsByOwnerId(workload.getId());
+
+ if (!pods.length) return;
+
+ const selectedPod = pods[0];
+ const selectedContainer = selectedPod.getAllContainers()[0];
+ const title = `${workload.kind} ${selectedPod.getName()}`;
+
+ this.createLogsTab(title, {
+ pods,
+ selectedPod,
+ selectedContainer
+ });
+ }
+
+ renameTab(tabId: string) {
+ const { selectedPod } = this.getData(tabId);
+
+ dockStore.renameTab(tabId, `Pod ${selectedPod.metadata.name}`);
+ }
+
+ private createDockTab(tabParams: Partial) {
+ dockStore.createTab({
+ kind: TabKind.POD_LOGS,
+ ...tabParams
+ }, false);
+ }
+
+ private createLogsTab(title: string, data: LogTabData) {
+ const id = uniqueId("log-tab-");
+
+ this.createDockTab({ id, title });
+ this.setData(id, {
+ ...data,
+ showTimestamps: false,
+ previous: false
+ });
+ }
+
+ private updateTabsData() {
+ this.data.forEach((tabData, tabId) => {
+ const pod = new Pod(tabData.selectedPod);
+ const pods = podsStore.getPodsByOwnerId(pod.getOwnerRefs()[0]?.uid);
+ const isSelectedPodInList = pods.find(item => item.getId() == pod.getId());
+ const selectedPod = isSelectedPodInList ? pod : pods[0];
+ const selectedContainer = isSelectedPodInList ? tabData.selectedContainer : pod.getAllContainers()[0];
+
+ if (pods.length) {
+ this.setData(tabId, {
+ ...tabData,
+ selectedPod,
+ selectedContainer,
+ pods
+ });
+
+ this.renameTab(tabId);
+ } else {
+ this.closeTab(tabId);
+ }
+ });
+ }
+
+ private closeTab(tabId: string) {
+ this.clearData(tabId);
+ dockStore.closeTab(tabId);
+ }
+}
+
+export const logTabStore = new LogTabStore();
+
+export function isLogsTab(tab: IDockTab) {
+ return tab && tab.kind === TabKind.POD_LOGS;
+}
diff --git a/src/renderer/components/dock/pod-logs.store.ts b/src/renderer/components/dock/log.store.ts
similarity index 62%
rename from src/renderer/components/dock/pod-logs.store.ts
rename to src/renderer/components/dock/log.store.ts
index 9e69a15b7f..14dc9efdd0 100644
--- a/src/renderer/components/dock/pod-logs.store.ts
+++ b/src/renderer/components/dock/log.store.ts
@@ -1,27 +1,16 @@
-import { autorun, computed, observable, reaction } from "mobx";
-import { Pod, IPodContainer, podsApi, IPodLogsQuery } from "../../api/endpoints";
+import { autorun, computed, observable } from "mobx";
+
+import { IPodLogsQuery, Pod, podsApi } from "../../api/endpoints";
import { autobind, interval } from "../../utils";
-import { DockTabStore } from "./dock-tab.store";
-import { dockStore, IDockTab, TabKind } from "./dock.store";
-import { searchStore } from "../../../common/search-store";
+import { dockStore, TabId } from "./dock.store";
+import { isLogsTab, logTabStore } from "./log-tab.store";
-export interface IPodLogsData {
- pod: Pod;
- selectedContainer: IPodContainer
- containers: IPodContainer[]
- initContainers: IPodContainer[]
- showTimestamps: boolean
- previous: boolean
-}
-
-type TabId = string;
type PodLogLine = string;
-// Number for log lines to load
-export const logRange = 500;
+const logLinesToLoad = 500;
@autobind()
-export class PodLogsStore extends DockTabStore {
+export class LogStore {
private refresher = interval(10, () => {
const id = dockStore.selectedTabId;
@@ -30,30 +19,17 @@ export class PodLogsStore extends DockTabStore {
});
@observable podLogs = observable.map();
- @observable newLogSince = observable.map(); // Timestamp after which all logs are considered to be new
constructor() {
- super({
- storageName: "pod_logs"
- });
autorun(() => {
const { selectedTab, isOpen } = dockStore;
- if (isPodLogsTab(selectedTab) && isOpen) {
+ if (isLogsTab(selectedTab) && isOpen) {
this.refresher.start();
} else {
this.refresher.stop();
}
}, { delay: 500 });
-
- reaction(() => this.podLogs.get(dockStore.selectedTabId), () => {
- this.setNewLogSince(dockStore.selectedTabId);
- });
-
- reaction(() => dockStore.selectedTabId, () => {
- // Clear search query on tab change
- searchStore.reset();
- });
}
/**
@@ -66,7 +42,7 @@ export class PodLogsStore extends DockTabStore {
load = async (tabId: TabId) => {
try {
const logs = await this.loadLogs(tabId, {
- tailLines: this.lines + logRange
+ tailLines: this.lines + logLinesToLoad
});
this.refresher.start();
@@ -107,9 +83,9 @@ export class PodLogsStore extends DockTabStore {
* @returns {Promise} A fetch request promise
*/
loadLogs = async (tabId: TabId, params: Partial) => {
- const data = this.getData(tabId);
+ const data = logTabStore.getData(tabId);
const { selectedContainer, previous } = data;
- const pod = new Pod(data.pod);
+ const pod = new Pod(data.selectedPod);
const namespace = pod.getNs();
const name = pod.getName();
@@ -127,17 +103,6 @@ export class PodLogsStore extends DockTabStore {
});
};
- /**
- * Sets newLogSince separator timestamp to split old logs from new ones
- * @param tabId
- */
- setNewLogSince(tabId: TabId) {
- if (!this.podLogs.has(tabId) || !this.podLogs.get(tabId).length || this.newLogSince.has(tabId)) return;
- const timestamp = this.getLastSinceTime(tabId);
-
- this.newLogSince.set(tabId, timestamp.split(".")[0]); // Removing milliseconds from string
- }
-
/**
* Converts logs into a string array
* @returns {number} Length of log lines
@@ -196,37 +161,6 @@ export class PodLogsStore extends DockTabStore {
clearLogs(tabId: TabId) {
this.podLogs.delete(tabId);
}
-
- clearData(tabId: TabId) {
- this.data.delete(tabId);
- this.clearLogs(tabId);
- }
}
-export const podLogsStore = new PodLogsStore();
-
-export function createPodLogsTab(data: IPodLogsData, tabParams: Partial = {}) {
- const podId = data.pod.getId();
- let tab = dockStore.getTabById(podId);
-
- if (tab) {
- dockStore.open();
- dockStore.selectTab(tab.id);
-
- return;
- }
- // If no existent tab found
- tab = dockStore.createTab({
- id: podId,
- kind: TabKind.POD_LOGS,
- title: data.pod.getName(),
- ...tabParams
- }, false);
- podLogsStore.setData(tab.id, data);
-
- return tab;
-}
-
-export function isPodLogsTab(tab: IDockTab) {
- return tab && tab.kind === TabKind.POD_LOGS;
-}
+export const logStore = new LogStore();
diff --git a/src/renderer/components/dock/pod-logs.tsx b/src/renderer/components/dock/logs.tsx
similarity index 53%
rename from src/renderer/components/dock/pod-logs.tsx
rename to src/renderer/components/dock/logs.tsx
index 696c2bf0ab..0aa31f95fb 100644
--- a/src/renderer/components/dock/pod-logs.tsx
+++ b/src/renderer/components/dock/logs.tsx
@@ -6,9 +6,12 @@ import { searchStore } from "../../../common/search-store";
import { autobind } from "../../utils";
import { IDockTab } from "./dock.store";
import { InfoPanel } from "./info-panel";
-import { PodLogControls } from "./pod-log-controls";
-import { PodLogList } from "./pod-log-list";
-import { IPodLogsData, podLogsStore } from "./pod-logs.store";
+import { LogResourceSelector } from "./log-resource-selector";
+import { LogList } from "./log-list";
+import { logStore } from "./log.store";
+import { LogSearch } from "./log-search";
+import { LogControls } from "./log-controls";
+import { LogTabData, logTabStore } from "./log-tab.store";
interface Props {
className?: string
@@ -16,10 +19,10 @@ interface Props {
}
@observer
-export class PodLogs extends React.Component {
+export class Logs extends React.Component {
@observable isLoading = true;
- private logListElement = React.createRef(); // A reference for VirtualList component
+ private logListElement = React.createRef(); // A reference for VirtualList component
componentDidMount() {
disposeOnUnmount(this,
@@ -28,7 +31,7 @@ export class PodLogs extends React.Component {
}
get tabData() {
- return podLogsStore.getData(this.tabId);
+ return logTabStore.getData(this.tabId);
}
get tabId() {
@@ -36,18 +39,18 @@ export class PodLogs extends React.Component {
}
@autobind()
- save(data: Partial) {
- podLogsStore.setData(this.tabId, { ...this.tabData, ...data });
+ save(data: Partial) {
+ logTabStore.setData(this.tabId, { ...this.tabData, ...data });
}
load = async () => {
this.isLoading = true;
- await podLogsStore.load(this.tabId);
+ await logStore.load(this.tabId);
this.isLoading = false;
};
reload = async () => {
- podLogsStore.clearLogs(this.tabId);
+ logStore.clearLogs(this.tabId);
await this.load();
};
@@ -79,39 +82,56 @@ export class PodLogs extends React.Component {
}, 100);
}
- render() {
- const logs = podLogsStore.logs;
-
+ renderResourceSelector() {
+ const logs = logStore.logs;
+ const searchLogs = this.tabData.showTimestamps ? logs : logStore.logsWithoutTimestamps;
const controls = (
-
+
+
+
+
);
+ return (
+
+ );
+ }
+
+ render() {
+ const logs = logStore.logs;
+
return (
);
}
diff --git a/src/renderer/components/dock/pod-log-controls.tsx b/src/renderer/components/dock/pod-log-controls.tsx
deleted file mode 100644
index bd6cb9db15..0000000000
--- a/src/renderer/components/dock/pod-log-controls.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-import "./pod-log-controls.scss";
-import React from "react";
-import { observer } from "mobx-react";
-import { IPodLogsData, podLogsStore } from "./pod-logs.store";
-import { Select, SelectOption } from "../select";
-import { Badge } from "../badge";
-import { Icon } from "../icon";
-import { cssNames, saveFileDialog } from "../../utils";
-import { Pod } from "../../api/endpoints";
-import { PodLogSearch, PodLogSearchProps } from "./pod-log-search";
-
-interface Props extends PodLogSearchProps {
- ready: boolean
- tabId: string
- tabData: IPodLogsData
- logs: string[]
- save: (data: Partial) => void
- reload: () => void
- onSearch: (query: string) => void
-}
-
-export const PodLogControls = observer((props: Props) => {
- const { tabData, save, reload, logs } = props;
- const { selectedContainer, showTimestamps, previous } = tabData;
- const since = logs.length ? podLogsStore.getTimestamps(logs[0]) : null;
- const pod = new Pod(tabData.pod);
-
- const toggleTimestamps = () => {
- save({ showTimestamps: !showTimestamps });
- };
-
- const togglePrevious = () => {
- save({ previous: !previous });
- reload();
- };
-
- const downloadLogs = () => {
- const fileName = selectedContainer ? selectedContainer.name : pod.getName();
- const logsToDownload = showTimestamps ? logs : podLogsStore.logsWithoutTimestamps;
-
- saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain");
- };
-
- const onContainerChange = (option: SelectOption) => {
- const { containers, initContainers } = tabData;
-
- save({
- selectedContainer: containers
- .concat(initContainers)
- .find(container => container.name === option.value)
- });
- reload();
- };
-
- const containerSelectOptions = () => {
- const { containers, initContainers } = tabData;
-
- return [
- {
- label: `Containers`,
- options: containers.map(container => {
- return { value: container.name };
- }),
- },
- {
- label: `Init Containers`,
- options: initContainers.map(container => {
- return { value: container.name };
- }),
- }
- ];
- };
-
- const formatOptionLabel = (option: SelectOption) => {
- const { value, label } = option;
-
- return label || <> {value}>;
- };
-
- return (
-
-
Pod:
- Namespace:
- Container
-
-
- {since && (
- <>
- Since{" "}
- {new Date(since[0]).toLocaleString()}
- >
- )}
-
-
-
- );
-});
diff --git a/src/renderer/components/dock/upgrade-chart.store.ts b/src/renderer/components/dock/upgrade-chart.store.ts
index bc7445e29d..63468f3180 100644
--- a/src/renderer/components/dock/upgrade-chart.store.ts
+++ b/src/renderer/components/dock/upgrade-chart.store.ts
@@ -80,7 +80,7 @@ export class UpgradeChartStore extends DockTabStore {
const values = this.values.getData(tabId);
await Promise.all([
- !releaseStore.isLoaded && releaseStore.loadAll(),
+ !releaseStore.isLoaded && releaseStore.loadFromContextNamespaces(),
!values && this.loadValues(tabId)
]);
}
diff --git a/src/renderer/components/dock/upgrade-chart.tsx b/src/renderer/components/dock/upgrade-chart.tsx
index 41ce5d295e..c5253cb5b1 100644
--- a/src/renderer/components/dock/upgrade-chart.tsx
+++ b/src/renderer/components/dock/upgrade-chart.tsx
@@ -123,8 +123,8 @@ export class UpgradeChart extends React.Component {
tabId={tabId}
error={error}
submit={upgrade}
- submitLabel={`Upgrade`}
- submittingMessage={`Updating..`}
+ submitLabel="Upgrade"
+ submittingMessage="Updating.."
controls={controlsAndInfo}
/>
-
+
{label}
{link}
diff --git a/src/renderer/components/drawer/drawer.scss b/src/renderer/components/drawer/drawer.scss
index b34b3b5965..8b7fc27993 100644
--- a/src/renderer/components/drawer/drawer.scss
+++ b/src/renderer/components/drawer/drawer.scss
@@ -69,7 +69,6 @@
padding: var(--spacing);
.Table .TableHead {
- background-color: $contentColor;
border-bottom: 1px solid $borderFaintColor;
}
}
diff --git a/src/renderer/components/editable-list/index.ts b/src/renderer/components/editable-list/index.ts
index cc0293acd6..1dc93d5df7 100644
--- a/src/renderer/components/editable-list/index.ts
+++ b/src/renderer/components/editable-list/index.ts
@@ -1 +1 @@
-export * from "./editable-list";
\ No newline at end of file
+export * from "./editable-list";
diff --git a/src/renderer/components/error-boundary/error-boundary.scss b/src/renderer/components/error-boundary/error-boundary.scss
index e50ee9d694..d139ce7ce9 100644
--- a/src/renderer/components/error-boundary/error-boundary.scss
+++ b/src/renderer/components/error-boundary/error-boundary.scss
@@ -4,6 +4,17 @@
padding: var(--flex-gap);
word-break: break-all;
+ .wrapper {
+ display: grid;
+ grid-template-columns: minmax(300px, 1fr) minmax(300px, 1fr);
+ column-gap: 12px;
+ row-gap: 12px;
+
+ @media screen and (max-width: 900px) {
+ grid-template-columns: auto;
+ }
+ }
+
code {
max-height: none;
border: 5px solid $borderFaintColor;
diff --git a/src/renderer/components/error-boundary/error-boundary.tsx b/src/renderer/components/error-boundary/error-boundary.tsx
index d0476bf225..a70e27ce3d 100644
--- a/src/renderer/components/error-boundary/error-boundary.tsx
+++ b/src/renderer/components/error-boundary/error-boundary.tsx
@@ -49,7 +49,7 @@ export class ErrorBoundary extends React.Component
{
To help us improve the product please report bugs to {slackLink} community or {githubLink} issues tracker.
-
+
Component stack:
{errorInfo.componentStack}
@@ -61,7 +61,7 @@ export class ErrorBoundary extends React.Component {
diff --git a/src/renderer/components/error-boundary/index.ts b/src/renderer/components/error-boundary/index.ts
index cdcf838466..90e954fb2e 100644
--- a/src/renderer/components/error-boundary/index.ts
+++ b/src/renderer/components/error-boundary/index.ts
@@ -1 +1 @@
-export * from "./error-boundary";
\ No newline at end of file
+export * from "./error-boundary";
diff --git a/src/renderer/components/file-picker/file-picker.tsx b/src/renderer/components/file-picker/file-picker.tsx
index 14cd6c07e8..1a52fe4973 100644
--- a/src/renderer/components/file-picker/file-picker.tsx
+++ b/src/renderer/components/file-picker/file-picker.tsx
@@ -209,4 +209,4 @@ export class FilePicker extends React.Component {
return ;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/file-picker/index.ts b/src/renderer/components/file-picker/index.ts
index f58aec1470..28c490afab 100644
--- a/src/renderer/components/file-picker/index.ts
+++ b/src/renderer/components/file-picker/index.ts
@@ -1 +1 @@
-export * from "./file-picker";
\ No newline at end of file
+export * from "./file-picker";
diff --git a/src/renderer/components/icon/index.ts b/src/renderer/components/icon/index.ts
index 5cdcefa69c..b975409af4 100644
--- a/src/renderer/components/icon/index.ts
+++ b/src/renderer/components/icon/index.ts
@@ -1 +1 @@
-export * from "./icon";
\ No newline at end of file
+export * from "./icon";
diff --git a/src/renderer/components/input/input.tsx b/src/renderer/components/input/input.tsx
index f2aafb92b8..ce0594c0e6 100644
--- a/src/renderer/components/input/input.tsx
+++ b/src/renderer/components/input/input.tsx
@@ -242,7 +242,7 @@ export class Input extends React.Component {
switch (evt.key) {
case "Enter":
- if (this.props.onSubmit && !modified && !evt.repeat) {
+ if (this.props.onSubmit && !modified && !evt.repeat && this.isValid) {
this.props.onSubmit(this.getValue());
}
break;
diff --git a/src/renderer/components/input/search-input-url.tsx b/src/renderer/components/input/search-input-url.tsx
index 2b1045ede2..c0e00d6e56 100644
--- a/src/renderer/components/input/search-input-url.tsx
+++ b/src/renderer/components/input/search-input-url.tsx
@@ -54,4 +54,4 @@ export class SearchInputUrl extends React.Component {
/>
);
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/item-object-list/index.tsx b/src/renderer/components/item-object-list/index.tsx
index b0b106b298..87ba0e908a 100644
--- a/src/renderer/components/item-object-list/index.tsx
+++ b/src/renderer/components/item-object-list/index.tsx
@@ -1 +1 @@
-export * from "./item-list-layout";
\ No newline at end of file
+export * from "./item-list-layout";
diff --git a/src/renderer/components/item-object-list/item-list-layout.scss b/src/renderer/components/item-object-list/item-list-layout.scss
index 9bdc2f943d..0008ffd527 100644
--- a/src/renderer/components/item-object-list/item-list-layout.scss
+++ b/src/renderer/components/item-object-list/item-list-layout.scss
@@ -36,3 +36,14 @@
}
}
+.ItemListLayoutVisibilityMenu {
+ .MenuItem {
+ padding: 0;
+ }
+
+ .Checkbox {
+ width: 100%;
+ padding: var(--spacing);
+ cursor: pointer;
+ }
+}
diff --git a/src/renderer/components/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx
index bdd6302f5a..895c275fbb 100644
--- a/src/renderer/components/item-object-list/item-list-layout.tsx
+++ b/src/renderer/components/item-object-list/item-list-layout.tsx
@@ -2,29 +2,33 @@ import "./item-list-layout.scss";
import groupBy from "lodash/groupBy";
import React, { ReactNode } from "react";
-import { computed, observable, reaction, toJS, when } from "mobx";
+import { computed, observable, reaction, toJS } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog";
-import { TableSortCallback, Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps } from "../table";
+import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallback } from "../table";
import { autobind, createStorage, cssNames, IClassName, isReactNode, noop, prevDefault, stopPropagation } from "../../utils";
import { AddRemoveButtons, AddRemoveButtonsProps } from "../add-remove-buttons";
import { NoItems } from "../no-items";
import { Spinner } from "../spinner";
import { ItemObject, ItemStore } from "../../item.store";
import { SearchInputUrl } from "../input";
-import { namespaceStore } from "../+namespaces/namespace.store";
import { Filter, FilterType, pageFilters } from "./page-filters.store";
import { PageFiltersList } from "./page-filters-list";
import { PageFiltersSelect } from "./page-filters-select";
-import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
+import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
import { themeStore } from "../../theme.store";
+import { MenuActions } from "../menu/menu-actions";
+import { MenuItem } from "../menu";
+import { Checkbox } from "../checkbox";
+import { userStore } from "../../../common/user-store";
+import { namespaceStore } from "../+namespaces/namespace.store";
// todo: refactor, split to small re-usable components
export type SearchFilter = (item: T) => string | number | (string | number)[];
export type ItemsFilter = (items: T[]) => T[];
-interface IHeaderPlaceholders {
+export interface IHeaderPlaceholders {
title: ReactNode;
search: ReactNode;
filters: ReactNode;
@@ -32,9 +36,12 @@ interface IHeaderPlaceholders {
}
export interface ItemListLayoutProps {
+ tableId?: string;
className: IClassName;
+ items?: T[];
store: ItemStore;
dependentStores?: ItemStore[];
+ preloadStores?: boolean;
isClusterScoped?: boolean;
hideFilters?: boolean;
searchFilters?: SearchFilter[];
@@ -50,6 +57,7 @@ export interface ItemListLayoutProps {
isReady?: boolean; // show loading indicator while not ready
isSelectable?: boolean; // show checkbox in rows for selecting items
isSearchable?: boolean; // apply search-filter & add search-input
+ isConfigurable?: boolean;
copyClassNameFromHeadCells?: boolean;
sortingCallbacks?: { [sortBy: string]: TableSortCallback };
tableProps?: Partial; // low-level table configuration
@@ -74,7 +82,9 @@ const defaultProps: Partial = {
showHeader: true,
isSearchable: true,
isSelectable: true,
+ isConfigurable: false,
copyClassNameFromHeadCells: true,
+ preloadStores: true,
dependentStores: [],
filterItems: [],
hasDetailsView: true,
@@ -90,9 +100,6 @@ interface ItemListLayoutUserSettings {
export class ItemListLayout extends React.Component {
static defaultProps = defaultProps as object;
- @observable isUnmounting = false;
-
- // default user settings (ui show-hide tweaks mostly)
@observable userSettings: ItemListLayoutUserSettings = {
showAppliedFilters: false,
};
@@ -111,28 +118,29 @@ export class ItemListLayout extends React.Component {
}
async componentDidMount() {
- const { store, dependentStores, isClusterScoped } = this.props;
- const stores = [store, ...dependentStores];
+ const { isClusterScoped, isConfigurable, tableId, preloadStores } = this.props;
- if (!isClusterScoped) stores.push(namespaceStore);
+ if (isConfigurable && !tableId) {
+ throw new Error("[ItemListLayout]: configurable list require props.tableId to be specified");
+ }
- try {
- stores.map(store => store.reset());
- await Promise.all(stores.map(store => store.loadAll()));
- const subscriptions = stores.map(store => store.subscribe());
+ if (preloadStores) {
+ this.loadStores();
- await when(() => this.isUnmounting);
- subscriptions.forEach(dispose => dispose()); // unsubscribe all
- } catch (error) {
- console.log("catched", error);
+ if (!isClusterScoped) {
+ disposeOnUnmount(this, [
+ namespaceStore.onContextChange(() => this.loadStores())
+ ]);
+ }
}
}
- componentWillUnmount() {
- this.isUnmounting = true;
- const { store, isSelectable } = this.props;
+ private loadStores() {
+ const { store, dependentStores } = this.props;
+ const stores = Array.from(new Set([store, ...dependentStores]));
- if (isSelectable) store.resetSelection();
+ // load context namespaces by default (see also: ` `)
+ stores.forEach(store => store.loadAll(namespaceStore.contextNamespaces));
}
private filterCallbacks: { [type: string]: ItemsFilter } = {
@@ -168,18 +176,12 @@ export class ItemListLayout extends React.Component {
};
@computed get isReady() {
- const { isReady, store } = this.props;
-
- return typeof isReady == "boolean" ? isReady : store.isLoaded;
+ return this.props.isReady ?? this.props.store.isLoaded;
}
@computed get filters() {
let { activeFilters } = pageFilters;
- const { isClusterScoped, isSearchable, searchFilters } = this.props;
-
- if (isClusterScoped) {
- activeFilters = activeFilters.filter(({ type }) => type !== FilterType.NAMESPACE);
- }
+ const { isSearchable, searchFilters } = this.props;
if (!(isSearchable && searchFilters)) {
activeFilters = activeFilters.filter(({ type }) => type !== FilterType.SEARCH);
@@ -213,7 +215,9 @@ export class ItemListLayout extends React.Component {
}
});
- return this.applyFilters(filterItems, allItems);
+ const items = this.props.items ?? allItems;
+
+ return this.applyFilters(filterItems, items);
}
@autobind()
@@ -247,20 +251,18 @@ export class ItemListLayout extends React.Component {
/>
)}
{
- renderTableContents(item)
- .map((content, index) => {
- const cellProps: TableCellProps = isReactNode(content) ? { children: content } : content;
+ renderTableContents(item).map((content, index) => {
+ const cellProps: TableCellProps = isReactNode(content) ? { children: content } : content;
+ const headCell = renderTableHeader?.[index];
- if (copyClassNameFromHeadCells && renderTableHeader) {
- const headCell = renderTableHeader[index];
-
- if (headCell) {
- cellProps.className = cssNames(cellProps.className, headCell.className);
- }
- }
+ if (copyClassNameFromHeadCells && headCell) {
+ cellProps.className = cssNames(cellProps.className, headCell.className);
+ }
+ if (!headCell || !this.isHiddenColumn(headCell)) {
return ;
- })
+ }
+ })
}
{renderItemMenu && (
@@ -280,8 +282,8 @@ export class ItemListLayout extends React.Component {
const dialogCustomProps = customizeRemoveDialog ? customizeRemoveDialog(selectedItems) : {};
const selectedCount = selectedItems.length;
const tailCount = selectedCount > visibleMaxNamesCount ? selectedCount - visibleMaxNamesCount : 0;
- const tail = tailCount > 0 ? "and {tailCount} more" : null;
- const message = selectedCount <= 1 ? Remove item {selectedNames} ?
: Remove {selectedCount} items {selectedNames} {tail}?
;
+ const tail = tailCount > 0 ? <>, and {tailCount} more> : null;
+ const message = selectedCount <= 1 ? Remove item {selectedNames} ?
: Remove {selectedCount} items {selectedNames} {tail}?
;
ConfirmDialog.open({
ok: removeSelectedItems,
@@ -299,16 +301,11 @@ export class ItemListLayout extends React.Component {
return;
}
- return ;
+ return ;
}
renderNoItems() {
- const { allItems, items, filters } = this;
- const allItemsCount = allItems.length;
- const itemsCount = items.length;
- const isFiltered = filters.length > 0 && allItemsCount > itemsCount;
-
- if (isFiltered) {
+ if (this.filters.length > 0) {
return (
No items found.
@@ -321,7 +318,7 @@ export class ItemListLayout extends React.Component {
);
}
- return ;
+ return ;
}
renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode {
@@ -340,8 +337,8 @@ export class ItemListLayout extends React.Component {
}
renderInfo() {
- const { allItems, items, isReady, userSettings, filters } = this;
- const allItemsCount = allItems.length;
+ const { items, isReady, userSettings, filters } = this;
+ const allItemsCount = this.props.store.getTotalCount();
const itemsCount = items.length;
const isFiltered = isReady && filters.length > 0;
@@ -365,17 +362,17 @@ export class ItemListLayout extends React.Component {
title: {title} ,
info: this.renderInfo(),
filters: <>
- {!isClusterScoped && }
+ {!isClusterScoped && }
+ }}/>
>,
- search: ,
+ search: ,
};
let header = this.renderHeaderContent(placeholders);
if (customizeHeader) {
- const modifiedHeader = customizeHeader(placeholders, header);
+ const modifiedHeader = customizeHeader(placeholders, header) ?? {};
if (isReactNode(modifiedHeader)) {
header = modifiedHeader;
@@ -394,10 +391,38 @@ export class ItemListLayout extends React.Component {
);
}
+ renderTableHeader() {
+ const { renderTableHeader, isSelectable, isConfigurable, store } = this.props;
+
+ if (!renderTableHeader) {
+ return;
+ }
+
+ return (
+
+ {isSelectable && (
+ store.toggleSelectionAll(this.items))}
+ />
+ )}
+ {renderTableHeader.map((cellProps, index) => {
+ if (!this.isHiddenColumn(cellProps)) {
+ return ;
+ }
+ })}
+
+ {isConfigurable && this.renderColumnVisibilityMenu()}
+
+
+ );
+ }
+
renderList() {
const {
- isSelectable, tableProps = {}, renderTableHeader, renderItemMenu,
- store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem
+ store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem,
+ tableProps = {},
} = this.props;
const { isReady, removeItemsDialog, items } = this;
const { selectedItems } = store;
@@ -406,7 +431,7 @@ export class ItemListLayout extends React.Component {
return (
{!isReady && (
-
+
)}
{isReady && (
{
className: cssNames("box grow", tableProps.className, themeStore.activeTheme.type),
})}
>
- {renderTableHeader && (
-
- {isSelectable && (
- store.toggleSelectionAll(items))}
- />
- )}
- {renderTableHeader.map((cellProps, index) => )}
- {renderItemMenu && }
-
- )}
+ {this.renderTableHeader()}
{
!virtual && items.map(item => this.getRow(item.getId()))
}
+
)}
{
);
}
+ @computed get hiddenColumns() {
+ return userStore.getHiddenTableColumns(this.props.tableId);
+ }
+
+ isHiddenColumn({ id: columnId, showWithColumn }: TableCellProps): boolean {
+ if (!this.props.isConfigurable) {
+ return false;
+ }
+
+ return this.hiddenColumns.has(columnId) || (
+ showWithColumn && this.hiddenColumns.has(showWithColumn)
+ );
+ }
+
+ updateColumnVisibility({ id: columnId }: TableCellProps, isVisible: boolean) {
+ const hiddenColumns = new Set(this.hiddenColumns);
+
+ if (!isVisible) {
+ hiddenColumns.add(columnId);
+ } else {
+ hiddenColumns.delete(columnId);
+ }
+
+ userStore.setHiddenTableColumns(this.props.tableId, hiddenColumns);
+ }
+
+ renderColumnVisibilityMenu() {
+ const { renderTableHeader } = this.props;
+
+ return (
+
+ {renderTableHeader.map((cellProps, index) => (
+ !cellProps.showWithColumn && (
+
+ `}
+ value={!this.isHiddenColumn(cellProps)}
+ onChange={isVisible => this.updateColumnVisibility(cellProps, isVisible)}
+ />
+
+ )
+ ))}
+
+ );
+ }
+
renderFooter() {
if (this.props.renderFooter) {
return this.props.renderFooter(this);
diff --git a/src/renderer/components/item-object-list/page-filters.store.ts b/src/renderer/components/item-object-list/page-filters.store.ts
index 9bff008aa6..933a94c06b 100644
--- a/src/renderer/components/item-object-list/page-filters.store.ts
+++ b/src/renderer/components/item-object-list/page-filters.store.ts
@@ -1,6 +1,5 @@
import { computed, observable, reaction } from "mobx";
import { autobind } from "../../utils";
-import { namespaceStore } from "../+namespaces/namespace.store";
import { searchUrlParam } from "../input/search-input-url";
export enum FilterType {
@@ -24,32 +23,6 @@ export class PageFiltersStore {
constructor() {
this.syncWithGlobalSearch();
- this.syncWithContextNamespace();
- }
-
- protected syncWithContextNamespace() {
- const disposers = [
- reaction(() => this.getValues(FilterType.NAMESPACE), filteredNs => {
- if (filteredNs.length !== namespaceStore.contextNs.length) {
- namespaceStore.setContext(filteredNs);
- }
- }),
- reaction(() => namespaceStore.contextNs.toJS(), contextNs => {
- const filteredNs = this.getValues(FilterType.NAMESPACE);
- const isChanged = contextNs.length !== filteredNs.length;
-
- if (isChanged) {
- this.filters.replace([
- ...this.filters.filter(({ type }) => type !== FilterType.NAMESPACE),
- ...contextNs.map(ns => ({ type: FilterType.NAMESPACE, value: ns })),
- ]);
- }
- }, {
- fireImmediately: true
- })
- ];
-
- return () => disposers.forEach(dispose => dispose());
}
protected syncWithGlobalSearch() {
diff --git a/src/renderer/components/kube-object-status-icon/index.ts b/src/renderer/components/kube-object-status-icon/index.ts
index 36751596a0..3ef2e6b29c 100644
--- a/src/renderer/components/kube-object-status-icon/index.ts
+++ b/src/renderer/components/kube-object-status-icon/index.ts
@@ -1 +1 @@
-export * from "./kube-object-status-icon";
\ No newline at end of file
+export * from "./kube-object-status-icon";
diff --git a/src/renderer/components/kube-object-status-icon/kube-object-status-icon.tsx b/src/renderer/components/kube-object-status-icon/kube-object-status-icon.tsx
index 2fdb9bff6f..386255d1eb 100644
--- a/src/renderer/components/kube-object-status-icon/kube-object-status-icon.tsx
+++ b/src/renderer/components/kube-object-status-icon/kube-object-status-icon.tsx
@@ -48,7 +48,7 @@ export class KubeObjectStatusIcon extends React.Component {
getAge(timestamp: string) {
if (!timestamp) return "";
- const diff = new Date().getTime() - new Date(timestamp).getTime();
+ const diff = Date.now() - new Date(timestamp).getTime();
return formatDuration(diff, true);
}
diff --git a/src/renderer/components/kube-object/kube-object-details.tsx b/src/renderer/components/kube-object/kube-object-details.tsx
index 40a247c2d8..b07b0753fb 100644
--- a/src/renderer/components/kube-object/kube-object-details.tsx
+++ b/src/renderer/components/kube-object/kube-object-details.tsx
@@ -13,11 +13,21 @@ import { CrdResourceDetails } from "../+custom-resources";
import { KubeObjectMenu } from "./kube-object-menu";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
+/**
+ * Used to store `object.selfLink` to show more info about resource in the details panel.
+ */
export const kubeDetailsUrlParam = createPageParam({
name: "kube-details",
isSystem: true,
});
+/**
+ * Used to highlight last active/selected table row with the resource.
+ *
+ * @example
+ * If we go to "Nodes (page) -> Node (details) -> Pod (details)",
+ * last clicked Node should be "active" while Pod details are shown).
+ */
export const kubeSelectedUrlParam = createPageParam({
name: "kube-selected",
isSystem: true,
@@ -26,8 +36,8 @@ export const kubeSelectedUrlParam = createPageParam({
}
});
-export function showDetails(details = "", resetSelected = true) {
- const detailsUrl = getDetailsUrl(details, resetSelected);
+export function showDetails(selfLink = "", resetSelected = true) {
+ const detailsUrl = getDetailsUrl(selfLink, resetSelected);
navigation.merge({ search: detailsUrl });
}
@@ -36,18 +46,18 @@ export function hideDetails() {
showDetails();
}
-export function getDetailsUrl(details: string, resetSelected = false) {
- const detailsUrl = kubeDetailsUrlParam.toSearchString({ value: details });
+export function getDetailsUrl(selfLink: string, resetSelected = false, mergeGlobals = true) {
+ const params = new URLSearchParams(mergeGlobals ? navigation.searchParams : "");
+
+ params.set(kubeDetailsUrlParam.urlName, selfLink);
if (resetSelected) {
- const params = new URLSearchParams(detailsUrl);
-
- params.delete(kubeSelectedUrlParam.name);
-
- return `?${params.toString()}`;
+ params.delete(kubeSelectedUrlParam.urlName);
+ } else {
+ params.set(kubeSelectedUrlParam.urlName, kubeSelectedUrlParam.get());
}
- return detailsUrl;
+ return `?${params}`;
}
export interface KubeObjectDetailsProps {
diff --git a/src/renderer/components/kube-object/kube-object-list-layout.tsx b/src/renderer/components/kube-object/kube-object-list-layout.tsx
index 25922f0f72..0ecb602c16 100644
--- a/src/renderer/components/kube-object/kube-object-list-layout.tsx
+++ b/src/renderer/components/kube-object/kube-object-list-layout.tsx
@@ -1,15 +1,18 @@
import React from "react";
import { computed } from "mobx";
-import { observer } from "mobx-react";
+import { disposeOnUnmount, observer } from "mobx-react";
import { cssNames } from "../../utils";
import { KubeObject } from "../../api/kube-object";
import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-list-layout";
import { KubeObjectStore } from "../../kube-object.store";
import { KubeObjectMenu } from "./kube-object-menu";
import { kubeSelectedUrlParam, showDetails } from "./kube-object-details";
+import { kubeWatchApi } from "../../api/kube-watch-api";
+import { clusterContext } from "../context";
export interface KubeObjectListLayoutProps extends ItemListLayoutProps {
store: KubeObjectStore;
+ dependentStores?: KubeObjectStore[];
}
@observer
@@ -18,6 +21,18 @@ export class KubeObjectListLayout extends React.Component {
if (this.props.onDetails) {
this.props.onDetails(item);
@@ -27,12 +42,15 @@ export class KubeObjectListLayout extends React.Component {
diff --git a/src/renderer/components/kubeconfig-dialog/index.ts b/src/renderer/components/kubeconfig-dialog/index.ts
index fdd244fe98..cb8c90cc14 100644
--- a/src/renderer/components/kubeconfig-dialog/index.ts
+++ b/src/renderer/components/kubeconfig-dialog/index.ts
@@ -1 +1 @@
-export * from "./kubeconfig-dialog";
\ No newline at end of file
+export * from "./kubeconfig-dialog";
diff --git a/src/renderer/components/layout/__test__/main-layout-header.test.tsx b/src/renderer/components/layout/__test__/main-layout-header.test.tsx
index b2a7bb5d93..499839072c 100644
--- a/src/renderer/components/layout/__test__/main-layout-header.test.tsx
+++ b/src/renderer/components/layout/__test__/main-layout-header.test.tsx
@@ -46,4 +46,4 @@ describe(" ", () => {
expect(getByText("minikube")).toBeInTheDocument();
});
-});
\ No newline at end of file
+});
diff --git a/src/renderer/components/layout/login-layout.tsx b/src/renderer/components/layout/login-layout.tsx
index 8aa9c08e0b..669f783769 100755
--- a/src/renderer/components/layout/login-layout.tsx
+++ b/src/renderer/components/layout/login-layout.tsx
@@ -34,4 +34,4 @@ export class LoginLayout extends React.Component {
);
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/layout/main-layout.scss b/src/renderer/components/layout/main-layout.scss
index 92f1173b7a..4e456e357f 100755
--- a/src/renderer/components/layout/main-layout.scss
+++ b/src/renderer/components/layout/main-layout.scss
@@ -44,6 +44,11 @@
> main {
display: contents;
+
+ > * {
+ grid-area: main;
+ overflow: auto;
+ }
}
footer {
diff --git a/src/renderer/components/layout/sidebar-context.ts b/src/renderer/components/layout/sidebar-context.ts
index fff192cba3..7001bbc319 100644
--- a/src/renderer/components/layout/sidebar-context.ts
+++ b/src/renderer/components/layout/sidebar-context.ts
@@ -4,4 +4,4 @@ export const SidebarContext = React.createContext({ pinned:
export type SidebarContextValue = {
pinned: boolean;
-};
\ No newline at end of file
+};
diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx
index b44dd4a759..f47ba1702e 100644
--- a/src/renderer/components/layout/sidebar.tsx
+++ b/src/renderer/components/layout/sidebar.tsx
@@ -40,9 +40,7 @@ interface Props {
@observer
export class Sidebar extends React.Component {
async componentDidMount() {
- if (!crdStore.isLoaded && isAllowedResource("customresourcedefinitions")) {
- crdStore.loadAll();
- }
+ crdStore.reloadAll();
}
renderCustomResources() {
diff --git a/src/renderer/components/line-progress/index.ts b/src/renderer/components/line-progress/index.ts
index 91942d706a..bd76106dbb 100644
--- a/src/renderer/components/line-progress/index.ts
+++ b/src/renderer/components/line-progress/index.ts
@@ -1 +1 @@
-export * from "./line-progress";
\ No newline at end of file
+export * from "./line-progress";
diff --git a/src/renderer/components/markdown-viewer/index.ts b/src/renderer/components/markdown-viewer/index.ts
index e82c6ba3c3..3c42af15f4 100644
--- a/src/renderer/components/markdown-viewer/index.ts
+++ b/src/renderer/components/markdown-viewer/index.ts
@@ -1 +1 @@
-export * from "./markdown-viewer";
\ No newline at end of file
+export * from "./markdown-viewer";
diff --git a/src/renderer/components/markdown-viewer/markdown-viewer.tsx b/src/renderer/components/markdown-viewer/markdown-viewer.tsx
index b1a2334b97..08478cb5a9 100644
--- a/src/renderer/components/markdown-viewer/markdown-viewer.tsx
+++ b/src/renderer/components/markdown-viewer/markdown-viewer.tsx
@@ -34,4 +34,4 @@ export class MarkdownViewer extends Component {
/>
);
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/menu/menu-actions.tsx b/src/renderer/components/menu/menu-actions.tsx
index aa8191dd7f..96a88bdf88 100644
--- a/src/renderer/components/menu/menu-actions.tsx
+++ b/src/renderer/components/menu/menu-actions.tsx
@@ -13,6 +13,7 @@ import isString from "lodash/isString";
export interface MenuActionsProps extends Partial {
className?: string;
toolbar?: boolean; // display menu as toolbar with icons
+ autoCloseOnSelect?: boolean;
triggerIcon?: string | IconProps | React.ReactNode;
removeConfirmationMessage?: React.ReactNode | (() => React.ReactNode);
updateAction?(): void;
@@ -80,7 +81,7 @@ export class MenuActions extends React.Component {
render() {
const {
- className, toolbar, children, updateAction, removeAction, triggerIcon, removeConfirmationMessage,
+ className, toolbar, autoCloseOnSelect, children, updateAction, removeAction, triggerIcon, removeConfirmationMessage,
...menuProps
} = this.props;
const menuClassName = cssNames("MenuActions flex", className, {
@@ -98,20 +99,20 @@ export class MenuActions extends React.Component {
className={menuClassName}
usePortal={autoClose}
closeOnScroll={autoClose}
- closeOnClickItem={autoClose}
+ closeOnClickItem={autoCloseOnSelect ?? autoClose }
closeOnClickOutside={autoClose}
{...menuProps}
>
{children}
{updateAction && (
-
+
Edit
)}
{removeAction && (
-
+
Remove
)}
diff --git a/src/renderer/components/notifications/notifications.scss b/src/renderer/components/notifications/notifications.scss
index 37d4990ee5..7388abb1e8 100644
--- a/src/renderer/components/notifications/notifications.scss
+++ b/src/renderer/components/notifications/notifications.scss
@@ -15,7 +15,7 @@
.notification {
flex: 0 0;
- padding: $padding;
+ padding: $padding * 1.5;
border-radius: 3px;
min-width: 350px;
max-width: 35vw;
@@ -42,5 +42,9 @@
box-shadow: 0 0 20px $boxShadow;
}
}
+
+ .close {
+ margin-top: -2px;
+ }
}
}
diff --git a/src/renderer/components/notifications/notifications.store.ts b/src/renderer/components/notifications/notifications.store.tsx
similarity index 95%
rename from src/renderer/components/notifications/notifications.store.ts
rename to src/renderer/components/notifications/notifications.store.tsx
index 55549b9066..45c1eb9a6b 100644
--- a/src/renderer/components/notifications/notifications.store.ts
+++ b/src/renderer/components/notifications/notifications.store.tsx
@@ -18,6 +18,7 @@ export interface Notification {
message: NotificationMessage;
status?: NotificationStatus;
timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide
+ onClose?(): void; // additonal logic on when the notification times out or is closed by the "x"
}
@autobind()
diff --git a/src/renderer/components/notifications/notifications.tsx b/src/renderer/components/notifications/notifications.tsx
index 4ab297e289..0c1ac692cf 100644
--- a/src/renderer/components/notifications/notifications.tsx
+++ b/src/renderer/components/notifications/notifications.tsx
@@ -72,23 +72,26 @@ export class Notifications extends React.Component {
return (
this.elem = e}>
{notifications.map(notification => {
- const { id, status } = notification;
+ const { id, status, onClose } = notification;
const msgText = this.getMessage(notification);
return (
addAutoHideTimer(id)}
onMouseEnter={() => removeAutoHideTimer(id)}>
-
+
{msgText}
-
+
remove(id))}
+ onClick={prevDefault(() => {
+ remove(id);
+ onClose?.();
+ })}
/>
diff --git a/src/renderer/components/radio/index.ts b/src/renderer/components/radio/index.ts
index 577923ef9c..0df0f30e50 100644
--- a/src/renderer/components/radio/index.ts
+++ b/src/renderer/components/radio/index.ts
@@ -1 +1 @@
-export * from "./radio";
\ No newline at end of file
+export * from "./radio";
diff --git a/src/renderer/components/resource-metrics/index.ts b/src/renderer/components/resource-metrics/index.ts
index 5438f760b4..a50f74ea8e 100644
--- a/src/renderer/components/resource-metrics/index.ts
+++ b/src/renderer/components/resource-metrics/index.ts
@@ -1,2 +1,2 @@
export * from "./resource-metrics";
-export * from "./resource-metrics-text";
\ No newline at end of file
+export * from "./resource-metrics-text";
diff --git a/src/renderer/components/select/select.scss b/src/renderer/components/select/select.scss
index f3fd6e47b0..445e59d003 100644
--- a/src/renderer/components/select/select.scss
+++ b/src/renderer/components/select/select.scss
@@ -3,11 +3,9 @@
html {
$menuBackgroundColor: $contentColor;
- $menuSelectedOptionBgc: $layoutBackground;
--select-menu-bgc: #{$menuBackgroundColor};
--select-menu-border-color: #{$halfGray};
- --select-option-selected-bgc: #{$menuSelectedOptionBgc};
--select-option-selected-color: #{$selectOptionHoveredColor};
--select-option-focused-bgc: #{$colorInfo};
--select-option-focused-color: #{$textColorAccent};
@@ -95,7 +93,7 @@ html {
}
&--is-selected {
- background: var(--select-option-selected-bgc);
+ background: var(--menuSelectedOptionBgc);
color: var(--select-option-selected-color);
}
@@ -148,7 +146,6 @@ html {
&.theme-light {
--select-menu-bgc: white;
--select-option-selected-color: $textColorSecondary;
- --select-option-selected-bgc: $textColorSecondary;
.Select {
&__multi-value {
diff --git a/src/renderer/components/slider/index.ts b/src/renderer/components/slider/index.ts
index bc79daa3ff..67c45bb063 100644
--- a/src/renderer/components/slider/index.ts
+++ b/src/renderer/components/slider/index.ts
@@ -1 +1 @@
-export * from "./slider";
\ No newline at end of file
+export * from "./slider";
diff --git a/src/renderer/components/status-brick/index.ts b/src/renderer/components/status-brick/index.ts
index e16a2a8093..cc6d3e8879 100644
--- a/src/renderer/components/status-brick/index.ts
+++ b/src/renderer/components/status-brick/index.ts
@@ -1 +1 @@
-export * from "./status-brick";
\ No newline at end of file
+export * from "./status-brick";
diff --git a/src/renderer/components/status-brick/status-brick.tsx b/src/renderer/components/status-brick/status-brick.tsx
index 34c835c9fa..ced04acca4 100644
--- a/src/renderer/components/status-brick/status-brick.tsx
+++ b/src/renderer/components/status-brick/status-brick.tsx
@@ -19,4 +19,4 @@ export class StatusBrick extends React.Component
{
/>
);
}
-}
\ No newline at end of file
+}
diff --git a/src/renderer/components/table/table-cell.tsx b/src/renderer/components/table/table-cell.tsx
index a42db4c2be..81e2f9f85f 100644
--- a/src/renderer/components/table/table-cell.tsx
+++ b/src/renderer/components/table/table-cell.tsx
@@ -9,12 +9,14 @@ import { Checkbox } from "../checkbox";
export type TableCellElem = React.ReactElement;
export interface TableCellProps extends React.DOMAttributes {
+ id?: string; // used for configuration visibility of columns
className?: string;
title?: ReactNode;
checkbox?: boolean; // render cell with a checkbox
isChecked?: boolean; // mark checkbox as checked or not
renderBoolean?: boolean; // show "true" or "false" for all of the children elements are "typeof boolean"
sortBy?: TableSortBy; // column name, must be same as key in sortable object
+ showWithColumn?: string // id of the column which follow same visibility rules
_sorting?: Partial; // sorting state, don't use this prop outside (!)
_sort?(sortBy: TableSortBy): void; // sort function, don't use this prop outside (!)
_nowrap?: boolean; // indicator, might come from parent , don't use this prop outside (!)
@@ -63,7 +65,7 @@ export class TableCell extends React.Component {
}
render() {
- const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, renderBoolean: displayBoolean, ...cellProps } = this.props;
+ const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, renderBoolean: displayBoolean, showWithColumn, ...cellProps } = this.props;
const classNames = cssNames("TableCell", className, {
checkbox,
nowrap: _nowrap,
@@ -72,7 +74,7 @@ export class TableCell extends React.Component {
const content = displayBooleans(displayBoolean, title || children);
return (
-
+
{this.renderCheckbox()}
{_nowrap ?
{content}
: content}
{this.renderSortIcon()}
diff --git a/src/renderer/components/table/table.tsx b/src/renderer/components/table/table.tsx
index 9b5d396e85..19bd5b03e5 100644
--- a/src/renderer/components/table/table.tsx
+++ b/src/renderer/components/table/table.tsx
@@ -16,6 +16,7 @@ export type TableSortBy = string;
export type TableOrderBy = "asc" | "desc" | string;
export type TableSortParams = { sortBy: TableSortBy; orderBy: TableOrderBy };
export type TableSortCallback
= (data: D) => string | number | (string | number)[];
+export type TableSortCallbacks = { [columnId: string]: TableSortCallback };
export interface TableProps extends React.DOMAttributes {
items?: ItemObject[]; // Raw items data
@@ -24,11 +25,11 @@ export interface TableProps extends React.DOMAttributes {
selectable?: boolean; // Highlight rows on hover
scrollable?: boolean; // Use scrollbar if content is bigger than parent's height
storageKey?: string; // Keep some data in localStorage & restore on page reload, e.g sorting params
- sortable?: {
- // Define sortable callbacks for every column in
- // @sortItem argument in the callback is an object, provided in
- [sortBy: string]: TableSortCallback;
- };
+ /**
+ * Define sortable callbacks for every column in
+ * @sortItem argument in the callback is an object, provided in
+ */
+ sortable?: TableSortCallbacks;
sortSyncWithUrl?: boolean; // sorting state is managed globally from url params
sortByDefault?: Partial; // default sorting params
onSort?: (params: TableSortParams) => void; // callback on sort change, default: global sync with url
diff --git a/src/renderer/components/virtual-list/index.ts b/src/renderer/components/virtual-list/index.ts
index 4e5b065f43..3fad81848e 100644
--- a/src/renderer/components/virtual-list/index.ts
+++ b/src/renderer/components/virtual-list/index.ts
@@ -1 +1 @@
-export * from "./virtual-list";
\ No newline at end of file
+export * from "./virtual-list";
diff --git a/src/renderer/components/wizard/index.ts b/src/renderer/components/wizard/index.ts
index b217e311a9..da693bd87f 100644
--- a/src/renderer/components/wizard/index.ts
+++ b/src/renderer/components/wizard/index.ts
@@ -1 +1 @@
-export * from "./wizard";
\ No newline at end of file
+export * from "./wizard";
diff --git a/src/renderer/hooks/useInterval.ts b/src/renderer/hooks/useInterval.ts
index d195fa279f..7ab604511b 100644
--- a/src/renderer/hooks/useInterval.ts
+++ b/src/renderer/hooks/useInterval.ts
@@ -16,4 +16,4 @@ export function useInterval(callback: () => void, delay: number) {
return () => clearInterval(id);
}, [delay]);
-}
\ No newline at end of file
+}
diff --git a/src/renderer/hooks/useOnUnmount.ts b/src/renderer/hooks/useOnUnmount.ts
index 5af04e39b1..a8b6fdd1b4 100644
--- a/src/renderer/hooks/useOnUnmount.ts
+++ b/src/renderer/hooks/useOnUnmount.ts
@@ -2,4 +2,4 @@ import { useEffect } from "react";
export function useOnUnmount(callback: () => void) {
useEffect(() => callback, []);
-}
\ No newline at end of file
+}
diff --git a/src/renderer/hooks/useStorage.ts b/src/renderer/hooks/useStorage.ts
index 2af730fec8..97b0588d29 100644
--- a/src/renderer/hooks/useStorage.ts
+++ b/src/renderer/hooks/useStorage.ts
@@ -10,4 +10,4 @@ export function useStorage(key: string, initialValue?: T, options?: IStorageH
};
return [storageValue, setValue] as [T, (value: T) => void];
-}
\ No newline at end of file
+}
diff --git a/src/renderer/ipc/index.tsx b/src/renderer/ipc/index.tsx
new file mode 100644
index 0000000000..b9644f7404
--- /dev/null
+++ b/src/renderer/ipc/index.tsx
@@ -0,0 +1,61 @@
+import React from "react";
+import { ipcRenderer, IpcRendererEvent } from "electron";
+import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg } from "../../common/ipc";
+import { Notifications, notificationsStore } from "../components/notifications";
+import { Button } from "../components/button";
+import { isMac } from "../../common/vars";
+import * as uuid from "uuid";
+
+function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void {
+ notificationsStore.remove(notificationId);
+ ipcRenderer.send(backchannel, data);
+}
+
+function RenderYesButtons(props: { backchannel: string, notificationId: string }) {
+ if (isMac) {
+ /**
+ * auto-updater's "installOnQuit" is not applicable for macOS as per their docs.
+ *
+ * See: https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/src/AppUpdater.ts#L27-L32
+ */
+ return sendToBackchannel(props.backchannel, props.notificationId, { doUpdate: true, now: true })} />;
+ }
+
+ return (
+ <>
+ sendToBackchannel(props.backchannel, props.notificationId, { doUpdate: true, now: true })} />
+ sendToBackchannel(props.backchannel, props.notificationId, { doUpdate: true, now: false })} />
+ >
+ );
+}
+
+function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, updateInfo]: UpdateAvailableFromMain): void {
+ const notificationId = uuid.v4();
+
+ Notifications.info(
+ (
+
+
Update Available
+
Version {updateInfo.version} of Lens IDE is now available. Would you like to update?
+
+
+ sendToBackchannel(backchannel, notificationId, { doUpdate: false })} />
+
+
+ ), {
+ id: notificationId,
+ onClose() {
+ sendToBackchannel(backchannel, notificationId, { doUpdate: false });
+ }
+ }
+ );
+}
+
+export function registerIpcHandlers() {
+ onCorrect({
+ source: ipcRenderer,
+ channel: UpdateAvailableChannel,
+ listener: UpdateAvailableHandler,
+ verifier: areArgsUpdateAvailableFromMain,
+ });
+}
diff --git a/src/renderer/item.store.ts b/src/renderer/item.store.ts
index 2105954d32..a9ac3179c9 100644
--- a/src/renderer/item.store.ts
+++ b/src/renderer/item.store.ts
@@ -9,7 +9,7 @@ export interface ItemObject {
@autobind()
export abstract class ItemStore {
- abstract loadAll(): Promise;
+ abstract loadAll(...args: any[]): Promise;
protected defaultSorting = (item: T) => item.getName();
@@ -22,11 +22,23 @@ export abstract class ItemStore {
return this.items.filter(item => this.selectedItemsIds.get(item.getId()));
}
+ public getItems(): T[] {
+ return this.items.toJS();
+ }
+
+ public getTotalCount(): number {
+ return this.items.length;
+ }
+
getByName(name: string, ...args: any[]): T;
getByName(name: string): T {
return this.items.find(item => item.getName() === name);
}
+ getIndexById(id: string): number {
+ return this.items.findIndex(item => item.getId() === id);
+ }
+
@action
protected sortItems(items: T[] = this.items, sorting?: ((item: T) => any)[], order?: "asc" | "desc"): T[] {
return orderBy(items, sorting || this.defaultSorting, order);
@@ -40,8 +52,7 @@ export abstract class ItemStore {
if (item) {
return item;
- }
- else {
+ } else {
const items = this.sortItems([...this.items, newItem]);
this.items.replace(items);
@@ -83,8 +94,7 @@ export abstract class ItemStore {
const index = this.items.findIndex(item => item === existingItem);
this.items.splice(index, 1, item);
- }
- else {
+ } else {
let items = [...this.items, item];
if (sortItems) items = this.sortItems(items);
@@ -130,8 +140,7 @@ export abstract class ItemStore {
toggleSelection(item: T) {
if (this.isSelected(item)) {
this.unselect(item);
- }
- else {
+ } else {
this.select(item);
}
}
@@ -142,8 +151,7 @@ export abstract class ItemStore {
if (allSelected) {
visibleItems.forEach(this.unselect);
- }
- else {
+ } else {
visibleItems.forEach(this.select);
}
}
diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts
index bb2fffd819..987112a25c 100644
--- a/src/renderer/kube-object.store.ts
+++ b/src/renderer/kube-object.store.ts
@@ -1,23 +1,51 @@
-import { action, observable, reaction } from "mobx";
+import type { ClusterContext } from "./components/context";
+
+import { action, computed, observable, reaction, when } from "mobx";
import { autobind } from "./utils";
-import { KubeObject } from "./api/kube-object";
-import { IKubeWatchEvent, kubeWatchApi } from "./api/kube-watch-api";
+import { KubeObject, KubeStatus } from "./api/kube-object";
+import { IKubeWatchEvent } from "./api/kube-watch-api";
import { ItemStore } from "./item.store";
import { apiManager } from "./api/api-manager";
-import { IKubeApiQueryParams, KubeApi } from "./api/kube-api";
+import { IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api";
import { KubeJsonApiData } from "./api/kube-json-api";
-import { getHostedCluster } from "../common/cluster-store";
+
+export interface KubeObjectStoreLoadingParams {
+ namespaces: string[];
+ api?: KubeApi;
+}
@autobind()
export abstract class KubeObjectStore extends ItemStore {
+ @observable static defaultContext: ClusterContext; // TODO: support multiple cluster contexts
+
abstract api: KubeApi;
public readonly limit?: number;
public readonly bufferSize: number = 50000;
+ private loadedNamespaces: string[] = [];
+
+ contextReady = when(() => Boolean(this.context));
constructor() {
super();
this.bindWatchEventsUpdater();
- kubeWatchApi.addListener(this, this.onWatchApiEvent);
+ }
+
+ get context(): ClusterContext {
+ return KubeObjectStore.defaultContext;
+ }
+
+ @computed get contextItems(): T[] {
+ const namespaces = this.context?.contextNamespaces ?? [];
+
+ return this.items.filter(item => {
+ const itemNamespace = item.getNs();
+
+ return !itemNamespace /* cluster-wide */ || namespaces.includes(itemNamespace);
+ });
+ }
+
+ getTotalCount(): number {
+ return this.contextItems.length;
}
get query(): IKubeApiQueryParams {
@@ -42,6 +70,10 @@ export abstract class KubeObjectStore extends ItemSt
}
}
+ getById(id: string) {
+ return this.items.find(item => item.getId() === id);
+ }
+
getByName(name: string, namespace?: string): T {
return this.items.find(item => {
return item.getName() === name && (
@@ -71,14 +103,30 @@ export abstract class KubeObjectStore