mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Release/v5.3.1 (#4465)
Co-authored-by: Roman <ixrock@gmail.com> Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com> Co-authored-by: Juho Heikka <juho.heikka@gmail.com> Co-authored-by: Jan Jansen <farodin91@users.noreply.github.com>
This commit is contained in:
parent
a007cbd9ce
commit
63256dcaf9
@ -3,7 +3,7 @@
|
|||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
"homepage": "https://github.com/lensapp/lens",
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "5.3.0",
|
"version": "5.3.1",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -215,8 +215,8 @@
|
|||||||
"mac-ca": "^1.0.6",
|
"mac-ca": "^1.0.6",
|
||||||
"marked": "^2.1.3",
|
"marked": "^2.1.3",
|
||||||
"md5-file": "^5.0.0",
|
"md5-file": "^5.0.0",
|
||||||
"mobx": "^6.3.0",
|
"mobx": "^6.3.7",
|
||||||
"mobx-observable-history": "^2.0.1",
|
"mobx-observable-history": "^2.0.3",
|
||||||
"mobx-react": "^7.2.1",
|
"mobx-react": "^7.2.1",
|
||||||
"mock-fs": "^4.14.0",
|
"mock-fs": "^4.14.0",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import path from "path";
|
|||||||
import Config from "conf";
|
import Config from "conf";
|
||||||
import type { Options as ConfOptions } from "conf/dist/source/types";
|
import type { Options as ConfOptions } from "conf/dist/source/types";
|
||||||
import { ipcMain, ipcRenderer } from "electron";
|
import { ipcMain, ipcRenderer } from "electron";
|
||||||
import { IReactionOptions, makeObservable, reaction, runInAction } from "mobx";
|
import { IEqualsComparer, makeObservable, reaction, runInAction } from "mobx";
|
||||||
import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
|
import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
|
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
|
||||||
@ -33,7 +33,10 @@ import { kebabCase } from "lodash";
|
|||||||
import { AppPaths } from "./app-paths";
|
import { AppPaths } from "./app-paths";
|
||||||
|
|
||||||
export interface BaseStoreParams<T> extends ConfOptions<T> {
|
export interface BaseStoreParams<T> extends ConfOptions<T> {
|
||||||
syncOptions?: IReactionOptions;
|
syncOptions?: {
|
||||||
|
fireImmediately?: boolean;
|
||||||
|
equals?: IEqualsComparer<T>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -29,7 +29,6 @@ export default function configurePackages() {
|
|||||||
// Docs: https://mobx.js.org/configuration.html
|
// Docs: https://mobx.js.org/configuration.html
|
||||||
Mobx.configure({
|
Mobx.configure({
|
||||||
enforceActions: "never",
|
enforceActions: "never",
|
||||||
isolateGlobalState: true,
|
|
||||||
|
|
||||||
// TODO: enable later (read more: https://mobx.js.org/migrating-from-4-or-5.html)
|
// TODO: enable later (read more: https://mobx.js.org/migrating-from-4-or-5.html)
|
||||||
// computedRequiresReaction: true,
|
// computedRequiresReaction: true,
|
||||||
|
|||||||
@ -230,6 +230,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
restackItems(from: number, to: number): void {
|
restackItems(from: number, to: number): void {
|
||||||
const { items } = this.getActive();
|
const { items } = this.getActive();
|
||||||
const source = items[from];
|
const source = items[from];
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { KubeJsonApi } from "../kube-json-api";
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import AbortController from "abort-controller";
|
import AbortController from "abort-controller";
|
||||||
import { delay } from "../../utils/delay";
|
import { delay } from "../../utils/delay";
|
||||||
|
import { PassThrough } from "stream";
|
||||||
|
|
||||||
class TestKubeObject extends KubeObject {
|
class TestKubeObject extends KubeObject {
|
||||||
static kind = "Pod";
|
static kind = "Pod";
|
||||||
@ -133,7 +134,10 @@ describe("KubeApi", () => {
|
|||||||
checkPreferredVersion: true,
|
checkPreferredVersion: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await kubeApi.get();
|
await kubeApi.get({
|
||||||
|
name: "foo",
|
||||||
|
namespace: "default",
|
||||||
|
});
|
||||||
expect(kubeApi.apiPrefix).toEqual("/apis");
|
expect(kubeApi.apiPrefix).toEqual("/apis");
|
||||||
expect(kubeApi.apiGroup).toEqual("networking.k8s.io");
|
expect(kubeApi.apiGroup).toEqual("networking.k8s.io");
|
||||||
});
|
});
|
||||||
@ -167,13 +171,15 @@ describe("KubeApi", () => {
|
|||||||
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
||||||
const kubeApi = new KubeApi({
|
const kubeApi = new KubeApi({
|
||||||
request,
|
request,
|
||||||
objectConstructor: KubeObject,
|
objectConstructor: Object.assign(KubeObject, { apiBase }),
|
||||||
apiBase,
|
|
||||||
fallbackApiBases: [fallbackApiBase],
|
fallbackApiBases: [fallbackApiBase],
|
||||||
checkPreferredVersion: true,
|
checkPreferredVersion: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await kubeApi.get();
|
await kubeApi.get({
|
||||||
|
name: "foo",
|
||||||
|
namespace: "default",
|
||||||
|
});
|
||||||
expect(kubeApi.apiPrefix).toEqual("/apis");
|
expect(kubeApi.apiPrefix).toEqual("/apis");
|
||||||
expect(kubeApi.apiGroup).toEqual("extensions");
|
expect(kubeApi.apiGroup).toEqual("extensions");
|
||||||
});
|
});
|
||||||
@ -298,21 +304,30 @@ describe("KubeApi", () => {
|
|||||||
|
|
||||||
describe("watch", () => {
|
describe("watch", () => {
|
||||||
let api: TestKubeApi;
|
let api: TestKubeApi;
|
||||||
|
let stream: PassThrough;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
api = new TestKubeApi({
|
api = new TestKubeApi({
|
||||||
request,
|
request,
|
||||||
objectConstructor: TestKubeObject,
|
objectConstructor: TestKubeObject,
|
||||||
});
|
});
|
||||||
|
stream = new PassThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
stream.end();
|
||||||
|
stream.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends a valid watch request", () => {
|
it("sends a valid watch request", () => {
|
||||||
const spy = jest.spyOn(request, "getResponse");
|
const spy = jest.spyOn(request, "getResponse");
|
||||||
|
|
||||||
(fetch as any).mockResponse(async () => {
|
(fetch as any).mockResponse(async () => {
|
||||||
return {};
|
return {
|
||||||
|
body: stream,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
api.watch({ namespace: "kube-system" });
|
api.watch({ namespace: "kube-system" });
|
||||||
expect(spy).toHaveBeenCalledWith("/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", expect.anything(), expect.anything());
|
expect(spy).toHaveBeenCalledWith("/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", expect.anything(), expect.anything());
|
||||||
});
|
});
|
||||||
@ -321,9 +336,11 @@ describe("KubeApi", () => {
|
|||||||
const spy = jest.spyOn(request, "getResponse");
|
const spy = jest.spyOn(request, "getResponse");
|
||||||
|
|
||||||
(fetch as any).mockResponse(async () => {
|
(fetch as any).mockResponse(async () => {
|
||||||
return {};
|
return {
|
||||||
|
body: stream,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
api.watch({ namespace: "kube-system", timeout: 60 });
|
api.watch({ namespace: "kube-system", timeout: 60 });
|
||||||
expect(spy).toHaveBeenCalledWith("/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", { query: { timeoutSeconds: 60 }}, expect.anything());
|
expect(spy).toHaveBeenCalledWith("/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", { query: { timeoutSeconds: 60 }}, expect.anything());
|
||||||
});
|
});
|
||||||
@ -336,11 +353,13 @@ describe("KubeApi", () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
return {};
|
return {
|
||||||
|
body: stream,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
|
||||||
api.watch({
|
api.watch({
|
||||||
namespace: "kube-system",
|
namespace: "kube-system",
|
||||||
timeout: 60,
|
timeout: 60,
|
||||||
@ -358,30 +377,32 @@ describe("KubeApi", () => {
|
|||||||
it("if request ended", (done) => {
|
it("if request ended", (done) => {
|
||||||
const spy = jest.spyOn(request, "getResponse");
|
const spy = jest.spyOn(request, "getResponse");
|
||||||
|
|
||||||
|
jest.spyOn(stream, "on").mockImplementation((eventName: string, callback: Function) => {
|
||||||
|
// End the request in 100ms.
|
||||||
|
if (eventName === "end") {
|
||||||
|
setTimeout(() => {
|
||||||
|
callback();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
});
|
||||||
|
|
||||||
// we need to mock using jest as jest-fetch-mock doesn't support mocking the body completely
|
// we need to mock using jest as jest-fetch-mock doesn't support mocking the body completely
|
||||||
jest.spyOn(global, "fetch").mockImplementation(async () => {
|
jest.spyOn(global, "fetch").mockImplementation(async () => {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
body: {
|
body: stream,
|
||||||
on: (eventName: string, callback: Function) => {
|
|
||||||
// End the request in 100ms.
|
|
||||||
if (eventName === "end") {
|
|
||||||
setTimeout(() => {
|
|
||||||
callback();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as any;
|
} as any;
|
||||||
});
|
});
|
||||||
|
|
||||||
api.watch({
|
api.watch({
|
||||||
namespace: "kube-system",
|
namespace: "kube-system",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
expect(spy).toHaveBeenCalledTimes(2);
|
expect(spy).toHaveBeenCalledTimes(2);
|
||||||
done();
|
done();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@ -391,11 +412,13 @@ describe("KubeApi", () => {
|
|||||||
const spy = jest.spyOn(request, "getResponse");
|
const spy = jest.spyOn(request, "getResponse");
|
||||||
|
|
||||||
(fetch as any).mockResponse(async () => {
|
(fetch as any).mockResponse(async () => {
|
||||||
return {};
|
return {
|
||||||
|
body: stream,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeoutSeconds = 1;
|
const timeoutSeconds = 1;
|
||||||
|
|
||||||
api.watch({
|
api.watch({
|
||||||
namespace: "kube-system",
|
namespace: "kube-system",
|
||||||
timeout: timeoutSeconds,
|
timeout: timeoutSeconds,
|
||||||
@ -403,7 +426,7 @@ describe("KubeApi", () => {
|
|||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
expect(spy).toHaveBeenCalledTimes(2);
|
expect(spy).toHaveBeenCalledTimes(2);
|
||||||
done();
|
done();
|
||||||
}, timeoutSeconds * 1000 * 1.2);
|
}, timeoutSeconds * 1000 * 1.2);
|
||||||
@ -412,25 +435,27 @@ describe("KubeApi", () => {
|
|||||||
it("retries only once if request ends and timeout is set", (done) => {
|
it("retries only once if request ends and timeout is set", (done) => {
|
||||||
const spy = jest.spyOn(request, "getResponse");
|
const spy = jest.spyOn(request, "getResponse");
|
||||||
|
|
||||||
|
jest.spyOn(stream, "on").mockImplementation((eventName: string, callback: Function) => {
|
||||||
|
// End the request in 100ms.
|
||||||
|
if (eventName === "end") {
|
||||||
|
setTimeout(() => {
|
||||||
|
callback();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
});
|
||||||
|
|
||||||
// we need to mock using jest as jest-fetch-mock doesn't support mocking the body completely
|
// we need to mock using jest as jest-fetch-mock doesn't support mocking the body completely
|
||||||
jest.spyOn(global, "fetch").mockImplementation(async () => {
|
jest.spyOn(global, "fetch").mockImplementation(async () => {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
body: {
|
body: stream,
|
||||||
on: (eventName: string, callback: Function) => {
|
|
||||||
// End the request in 100ms
|
|
||||||
if (eventName === "end") {
|
|
||||||
setTimeout(() => {
|
|
||||||
callback();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as any;
|
} as any;
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeoutSeconds = 0.5;
|
const timeoutSeconds = 0.5;
|
||||||
|
|
||||||
api.watch({
|
api.watch({
|
||||||
namespace: "kube-system",
|
namespace: "kube-system",
|
||||||
timeout: timeoutSeconds,
|
timeout: timeoutSeconds,
|
||||||
@ -438,7 +463,7 @@ describe("KubeApi", () => {
|
|||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
expect(spy).toHaveBeenCalledTimes(2);
|
expect(spy).toHaveBeenCalledTimes(2);
|
||||||
done();
|
done();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|||||||
@ -25,4 +25,5 @@ export interface ClusterContext {
|
|||||||
cluster?: Cluster;
|
cluster?: Cluster;
|
||||||
allNamespaces: string[]; // available / allowed namespaces from cluster.ts
|
allNamespaces: string[]; // available / allowed namespaces from cluster.ts
|
||||||
contextNamespaces: string[]; // selected by user (see: namespace-select.tsx)
|
contextNamespaces: string[]; // selected by user (see: namespace-select.tsx)
|
||||||
|
hasSelectedAll: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,6 @@
|
|||||||
|
|
||||||
// Base class for building all kubernetes apis
|
// Base class for building all kubernetes apis
|
||||||
|
|
||||||
import merge from "lodash/merge";
|
|
||||||
import { isFunction } from "lodash";
|
import { isFunction } from "lodash";
|
||||||
import { stringify } from "querystring";
|
import { stringify } from "querystring";
|
||||||
import { apiKubePrefix, isDevelopment } from "../../common/vars";
|
import { apiKubePrefix, isDevelopment } from "../../common/vars";
|
||||||
@ -221,6 +220,29 @@ const patchTypeHeaders: Record<KubeApiPatchType, string> = {
|
|||||||
"strategic": "application/strategic-merge-patch+json",
|
"strategic": "application/strategic-merge-patch+json",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ResourceDescriptor {
|
||||||
|
/**
|
||||||
|
* The name of the kubernetes resource
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The namespace that the resource lives in (if the resource is namespaced)
|
||||||
|
*
|
||||||
|
* Note: if not provided and the resource kind is namespaced, then this defaults to `"default"`
|
||||||
|
*/
|
||||||
|
namespace?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteResourceDescriptor extends ResourceDescriptor {
|
||||||
|
/**
|
||||||
|
* This determinines how child resources should be handled by kubernetes
|
||||||
|
*
|
||||||
|
* @default "Background"
|
||||||
|
*/
|
||||||
|
propagationPolicy?: PropagationPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
export class KubeApi<T extends KubeObject> {
|
export class KubeApi<T extends KubeObject> {
|
||||||
readonly kind: string;
|
readonly kind: string;
|
||||||
readonly apiBase: string;
|
readonly apiBase: string;
|
||||||
@ -273,14 +295,18 @@ export class KubeApi<T extends KubeObject> {
|
|||||||
*/
|
*/
|
||||||
private async getLatestApiPrefixGroup() {
|
private async getLatestApiPrefixGroup() {
|
||||||
// Note that this.options.apiBase is the "full" url, whereas this.apiBase is parsed
|
// Note that this.options.apiBase is the "full" url, whereas this.apiBase is parsed
|
||||||
const apiBases = [this.options.apiBase, ...this.options.fallbackApiBases];
|
const apiBases = [this.options.apiBase, this.objectConstructor.apiBase, ...this.options.fallbackApiBases];
|
||||||
|
|
||||||
for (const apiUrl of apiBases) {
|
for (const apiUrl of apiBases) {
|
||||||
// Split e.g. "/apis/extensions/v1beta1/ingresses" to parts
|
if (!apiUrl) {
|
||||||
const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = parseKubeApi(apiUrl);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Request available resources
|
|
||||||
try {
|
try {
|
||||||
|
// Split e.g. "/apis/extensions/v1beta1/ingresses" to parts
|
||||||
|
const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = parseKubeApi(apiUrl);
|
||||||
|
|
||||||
|
// Request available resources
|
||||||
const response = await this.request.get<IKubeResourceList>(`${apiPrefix}/${apiVersionWithGroup}`);
|
const response = await this.request.get<IKubeResourceList>(`${apiPrefix}/${apiVersionWithGroup}`);
|
||||||
|
|
||||||
// If the resource is found in the group, use this apiUrl
|
// If the resource is found in the group, use this apiUrl
|
||||||
@ -304,7 +330,7 @@ export class KubeApi<T extends KubeObject> {
|
|||||||
return await this.getLatestApiPrefixGroup();
|
return await this.getLatestApiPrefixGroup();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If valid API wasn't found, log the error and return defaults below
|
// If valid API wasn't found, log the error and return defaults below
|
||||||
logger.error(error);
|
logger.error(`[KUBE-API]: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,12 +381,12 @@ export class KubeApi<T extends KubeObject> {
|
|||||||
return this.list(params, { limit: 1 });
|
return this.list(params, { limit: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
getUrl({ name = "", namespace = "" } = {}, query?: Partial<IKubeApiQueryParams>) {
|
getUrl({ name, namespace = "default" }: Partial<ResourceDescriptor> = {}, query?: Partial<IKubeApiQueryParams>) {
|
||||||
const resourcePath = createKubeApiURL({
|
const resourcePath = createKubeApiURL({
|
||||||
apiPrefix: this.apiPrefix,
|
apiPrefix: this.apiPrefix,
|
||||||
apiVersion: this.apiVersionWithGroup,
|
apiVersion: this.apiVersionWithGroup,
|
||||||
resource: this.apiResource,
|
resource: this.apiResource,
|
||||||
namespace: this.isNamespaced ? namespace : undefined,
|
namespace: this.isNamespaced ? namespace ?? "default" : undefined,
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -438,10 +464,10 @@ export class KubeApi<T extends KubeObject> {
|
|||||||
throw new Error(`GET multiple request to ${url} returned not an array: ${JSON.stringify(parsed)}`);
|
throw new Error(`GET multiple request to ${url} returned not an array: ${JSON.stringify(parsed)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async get({ name = "", namespace = "default" } = {}, query?: IKubeApiQueryParams): Promise<T | null> {
|
async get(desc: ResourceDescriptor, query?: IKubeApiQueryParams): Promise<T | null> {
|
||||||
await this.checkPreferredVersion();
|
await this.checkPreferredVersion();
|
||||||
|
|
||||||
const url = this.getUrl({ namespace, name });
|
const url = this.getUrl(desc);
|
||||||
const res = await this.request.get(url, { query });
|
const res = await this.request.get(url, { query });
|
||||||
const parsed = this.parseResponse(res);
|
const parsed = this.parseResponse(res);
|
||||||
|
|
||||||
@ -452,19 +478,18 @@ export class KubeApi<T extends KubeObject> {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create({ name = "", namespace = "default" } = {}, data?: Partial<T>): Promise<T | null> {
|
async create({ name, namespace }: Partial<ResourceDescriptor>, data?: Partial<T>): Promise<T | null> {
|
||||||
await this.checkPreferredVersion();
|
await this.checkPreferredVersion();
|
||||||
|
|
||||||
const apiUrl = this.getUrl({ namespace });
|
const apiUrl = this.getUrl({ namespace });
|
||||||
const res = await this.request.post(apiUrl, {
|
const res = await this.request.post(apiUrl, {
|
||||||
data: merge({
|
data: {
|
||||||
kind: this.kind,
|
...data,
|
||||||
apiVersion: this.apiVersionWithGroup,
|
|
||||||
metadata: {
|
metadata: {
|
||||||
name,
|
name,
|
||||||
namespace,
|
namespace,
|
||||||
},
|
},
|
||||||
}, data),
|
},
|
||||||
});
|
});
|
||||||
const parsed = this.parseResponse(res);
|
const parsed = this.parseResponse(res);
|
||||||
|
|
||||||
@ -475,11 +500,19 @@ export class KubeApi<T extends KubeObject> {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update({ name = "", namespace = "default" } = {}, data?: Partial<T>): Promise<T | null> {
|
async update({ name, namespace }: ResourceDescriptor, data: Partial<T>): Promise<T | null> {
|
||||||
await this.checkPreferredVersion();
|
await this.checkPreferredVersion();
|
||||||
const apiUrl = this.getUrl({ namespace, name });
|
const apiUrl = this.getUrl({ namespace, name });
|
||||||
|
|
||||||
const res = await this.request.put(apiUrl, { data });
|
const res = await this.request.put(apiUrl, {
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
metadata: {
|
||||||
|
name,
|
||||||
|
namespace,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
const parsed = this.parseResponse(res);
|
const parsed = this.parseResponse(res);
|
||||||
|
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
@ -489,9 +522,9 @@ export class KubeApi<T extends KubeObject> {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async patch({ name = "", namespace = "default" } = {}, data?: Partial<T> | Patch, strategy: KubeApiPatchType = "strategic"): Promise<T | null> {
|
async patch(desc: ResourceDescriptor, data?: Partial<T> | Patch, strategy: KubeApiPatchType = "strategic"): Promise<T | null> {
|
||||||
await this.checkPreferredVersion();
|
await this.checkPreferredVersion();
|
||||||
const apiUrl = this.getUrl({ namespace, name });
|
const apiUrl = this.getUrl(desc);
|
||||||
|
|
||||||
const res = await this.request.patch(apiUrl, { data }, {
|
const res = await this.request.patch(apiUrl, { data }, {
|
||||||
headers: {
|
headers: {
|
||||||
@ -507,16 +540,15 @@ export class KubeApi<T extends KubeObject> {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete({ name = "", namespace = "default", propagationPolicy = "Background" }: { name: string, namespace?: string, propagationPolicy?: PropagationPolicy }) {
|
async delete({ propagationPolicy = "Background", ...desc }: DeleteResourceDescriptor) {
|
||||||
await this.checkPreferredVersion();
|
await this.checkPreferredVersion();
|
||||||
const apiUrl = this.getUrl({ namespace, name });
|
const apiUrl = this.getUrl(desc);
|
||||||
const reqInit = {
|
|
||||||
|
return this.request.del(apiUrl, {
|
||||||
query: {
|
query: {
|
||||||
propagationPolicy,
|
propagationPolicy,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
return this.request.del(apiUrl, reqInit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getWatchUrl(namespace = "", query: IKubeApiQueryParams = {}) {
|
getWatchUrl(namespace = "", query: IKubeApiQueryParams = {}) {
|
||||||
|
|||||||
@ -33,9 +33,8 @@ import type { RequestInit } from "node-fetch";
|
|||||||
import AbortController from "abort-controller";
|
import AbortController from "abort-controller";
|
||||||
import type { Patch } from "rfc6902";
|
import type { Patch } from "rfc6902";
|
||||||
|
|
||||||
export interface KubeObjectStoreLoadingParams<K extends KubeObject> {
|
export interface KubeObjectStoreLoadingParams {
|
||||||
namespaces: string[];
|
namespaces: string[];
|
||||||
api?: KubeApi<K>;
|
|
||||||
reqInit?: RequestInit;
|
reqInit?: RequestInit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,6 +62,11 @@ export interface KubeObjectStoreSubscribeParams {
|
|||||||
* being rejected with
|
* being rejected with
|
||||||
*/
|
*/
|
||||||
onLoadFailure?: (err: any) => void;
|
onLoadFailure?: (err: any) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional parent abort controller
|
||||||
|
*/
|
||||||
|
abortController?: AbortController;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T> {
|
export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T> {
|
||||||
@ -167,8 +171,8 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadItems({ namespaces, api, reqInit, onLoadFailure }: KubeObjectStoreLoadingParams<T>): Promise<T[]> {
|
protected async loadItems({ namespaces, reqInit, onLoadFailure }: KubeObjectStoreLoadingParams): Promise<T[]> {
|
||||||
if (!this.context?.cluster.isAllowedResource(api.kind)) {
|
if (!this.context?.cluster.isAllowedResource(this.api.kind)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,12 +180,12 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
&& this.context.cluster.accessibleNamespaces.length === 0
|
&& this.context.cluster.accessibleNamespaces.length === 0
|
||||||
&& this.context.allNamespaces.every(ns => namespaces.includes(ns));
|
&& this.context.allNamespaces.every(ns => namespaces.includes(ns));
|
||||||
|
|
||||||
if (!api.isNamespaced || isLoadingAll) {
|
if (!this.api.isNamespaced || isLoadingAll) {
|
||||||
if (api.isNamespaced) {
|
if (this.api.isNamespaced) {
|
||||||
this.loadedNamespaces = [];
|
this.loadedNamespaces = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = api.list({ reqInit }, this.query);
|
const res = this.api.list({ reqInit }, this.query);
|
||||||
|
|
||||||
if (onLoadFailure) {
|
if (onLoadFailure) {
|
||||||
try {
|
try {
|
||||||
@ -203,7 +207,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
this.loadedNamespaces = namespaces;
|
this.loadedNamespaces = namespaces;
|
||||||
|
|
||||||
const results = await Promise.allSettled(
|
const results = await Promise.allSettled(
|
||||||
namespaces.map(namespace => api.list({ namespace, reqInit }, this.query)),
|
namespaces.map(namespace => this.api.list({ namespace, reqInit }, this.query)),
|
||||||
);
|
);
|
||||||
const res: T[] = [];
|
const res: T[] = [];
|
||||||
|
|
||||||
@ -215,7 +219,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
|
|
||||||
case "rejected":
|
case "rejected":
|
||||||
if (onLoadFailure) {
|
if (onLoadFailure) {
|
||||||
onLoadFailure(result.reason.message);
|
onLoadFailure(result.reason.message || result.reason);
|
||||||
} else {
|
} else {
|
||||||
// if onLoadFailure is not provided then preserve old behaviour
|
// if onLoadFailure is not provided then preserve old behaviour
|
||||||
throw result.reason;
|
throw result.reason;
|
||||||
@ -231,24 +235,14 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll(options: KubeObjectStoreLoadAllParams = {}): Promise<void | T[]> {
|
async loadAll({ namespaces = this.context.contextNamespaces, merge = true, reqInit, onLoadFailure }: KubeObjectStoreLoadAllParams = {}): Promise<void | T[]> {
|
||||||
await this.contextReady;
|
await this.contextReady;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const {
|
|
||||||
namespaces = this.context.allNamespaces, // load all namespaces by default
|
|
||||||
merge = true, // merge loaded items or return as result
|
|
||||||
reqInit,
|
|
||||||
onLoadFailure,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const items = await this.loadItems({ namespaces, api: this.api, reqInit, onLoadFailure });
|
const items = await this.loadItems({ namespaces, reqInit, onLoadFailure });
|
||||||
|
|
||||||
if (merge) {
|
this.mergeItems(items, { merge });
|
||||||
this.mergeItems(items, { replace: false });
|
|
||||||
} else {
|
|
||||||
this.mergeItems(items, { replace: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
this.failedLoading = false;
|
this.failedLoading = false;
|
||||||
@ -275,11 +269,11 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
protected mergeItems(partialItems: T[], { replace = false, updateStore = true, sort = true, filter = true } = {}): T[] {
|
protected mergeItems(partialItems: T[], { merge = true, updateStore = true, sort = true, filter = true } = {}): T[] {
|
||||||
let items = partialItems;
|
let items = partialItems;
|
||||||
|
|
||||||
// update existing items
|
// update existing items
|
||||||
if (!replace) {
|
if (merge) {
|
||||||
const namespaces = partialItems.map(item => item.getNs());
|
const namespaces = partialItems.map(item => item.getNs());
|
||||||
|
|
||||||
items = [
|
items = [
|
||||||
@ -369,7 +363,8 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
return this.postUpdate(
|
return this.postUpdate(
|
||||||
await this.api.update(
|
await this.api.update(
|
||||||
{
|
{
|
||||||
name: item.getName(), namespace: item.getNs(),
|
name: item.getName(),
|
||||||
|
namespace: item.getNs(),
|
||||||
},
|
},
|
||||||
data,
|
data,
|
||||||
),
|
),
|
||||||
@ -394,23 +389,21 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(opts: KubeObjectStoreSubscribeParams = {}) {
|
subscribe({ onLoadFailure, abortController = new AbortController() }: KubeObjectStoreSubscribeParams = {}) {
|
||||||
const abortController = new AbortController();
|
|
||||||
|
|
||||||
if (this.api.isNamespaced) {
|
if (this.api.isNamespaced) {
|
||||||
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])])
|
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) {
|
if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) {
|
||||||
return this.watchNamespace("", abortController, opts);
|
return this.watchNamespace("", abortController, { onLoadFailure });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const namespace of this.loadedNamespaces) {
|
for (const namespace of this.loadedNamespaces) {
|
||||||
this.watchNamespace(namespace, abortController, opts);
|
this.watchNamespace(namespace, abortController, { onLoadFailure });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(noop); // ignore DOMExceptions
|
.catch(noop); // ignore DOMExceptions
|
||||||
} else {
|
} else {
|
||||||
this.watchNamespace("", abortController, opts);
|
this.watchNamespace("", abortController, { onLoadFailure });
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => abortController.abort();
|
return () => abortController.abort();
|
||||||
|
|||||||
@ -105,8 +105,9 @@ export class KubeCreationError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata, Status = any, Spec = any> implements ItemObject {
|
export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata, Status = any, Spec = any> implements ItemObject {
|
||||||
static readonly kind: string;
|
static readonly kind?: string;
|
||||||
static readonly namespaced: boolean;
|
static readonly namespaced?: boolean;
|
||||||
|
static readonly apiBase?: string;
|
||||||
|
|
||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
@ -215,7 +216,7 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
constructor(data: KubeJsonApiData) {
|
constructor(data: KubeJsonApiData) {
|
||||||
if (typeof data !== "object") {
|
if (typeof data !== "object") {
|
||||||
throw new TypeError(`Cannot create a KubeObject from ${typeof data}`);
|
throw new TypeError(`Cannot create a KubeObject from ${typeof data}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,9 +330,9 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
|||||||
* Perform a full update (or more specifically a replace)
|
* Perform a full update (or more specifically a replace)
|
||||||
*
|
*
|
||||||
* Note: this is brittle if `data` is not actually partial (but instead whole).
|
* Note: this is brittle if `data` is not actually partial (but instead whole).
|
||||||
* As fields such as `resourceVersion` will probably out of date. This is a
|
* As fields such as `resourceVersion` will probably out of date. This is a
|
||||||
* common race condition.
|
* common race condition.
|
||||||
*
|
*
|
||||||
* @deprecated use KubeApi.update instead
|
* @deprecated use KubeApi.update instead
|
||||||
*/
|
*/
|
||||||
async update(data: Partial<this>): Promise<KubeJsonApiData | null> {
|
async update(data: Partial<this>): Promise<KubeJsonApiData | null> {
|
||||||
|
|||||||
@ -25,32 +25,36 @@
|
|||||||
import type { KubeObjectStore } from "./kube-object.store";
|
import type { KubeObjectStore } from "./kube-object.store";
|
||||||
import type { ClusterContext } from "./cluster-context";
|
import type { ClusterContext } from "./cluster-context";
|
||||||
|
|
||||||
import plimit from "p-limit";
|
import { comparer, reaction } from "mobx";
|
||||||
import { comparer, observable, reaction, makeObservable } from "mobx";
|
import { disposer, Disposer, noop } from "../utils";
|
||||||
import { autoBind, disposer, Disposer, noop } from "../utils";
|
|
||||||
import type { KubeApi } from "./kube-api";
|
|
||||||
import type { KubeJsonApiData } from "./kube-json-api";
|
import type { KubeJsonApiData } from "./kube-json-api";
|
||||||
import { isDebugging, isProduction } from "../vars";
|
|
||||||
import type { KubeObject } from "./kube-object";
|
import type { KubeObject } from "./kube-object";
|
||||||
|
import AbortController from "abort-controller";
|
||||||
|
import { once } from "lodash";
|
||||||
|
import logger from "../logger";
|
||||||
|
|
||||||
|
class WrappedAbortController extends AbortController {
|
||||||
|
constructor(protected parent: AbortController) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
parent.signal.addEventListener("abort", () => {
|
||||||
|
this.abort();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface IKubeWatchEvent<T extends KubeJsonApiData> {
|
export interface IKubeWatchEvent<T extends KubeJsonApiData> {
|
||||||
type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR";
|
type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR";
|
||||||
object?: T;
|
object?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KubeWatchPreloadOptions {
|
export interface KubeWatchSubscribeStoreOptions {
|
||||||
/**
|
/**
|
||||||
* The namespaces to watch
|
* The namespaces to watch
|
||||||
* @default all-accessible
|
* @default all selected namespaces
|
||||||
*/
|
*/
|
||||||
namespaces?: string[];
|
namespaces?: string[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to skip loading if the store is already loaded
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
loadOnce?: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function that is called when listing fails. If set then blocks errors
|
* A function that is called when listing fails. If set then blocks errors
|
||||||
* being rejected with
|
* being rejected with
|
||||||
@ -58,123 +62,148 @@ interface KubeWatchPreloadOptions {
|
|||||||
onLoadFailure?: (err: any) => void;
|
onLoadFailure?: (err: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KubeWatchSubscribeStoreOptions extends KubeWatchPreloadOptions {
|
|
||||||
/**
|
|
||||||
* Whether to subscribe only after loading all stores
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
waitUntilLoaded?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to preload the stores before watching
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
preload?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IKubeWatchLog {
|
export interface IKubeWatchLog {
|
||||||
message: string | string[] | Error;
|
message: string | string[] | Error;
|
||||||
meta?: object;
|
meta?: object;
|
||||||
cssStyle?: string;
|
cssStyle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SubscribeStoreParams {
|
||||||
|
store: KubeObjectStore<KubeObject>;
|
||||||
|
parent: AbortController;
|
||||||
|
watchChanges: boolean;
|
||||||
|
namespaces: string[];
|
||||||
|
onLoadFailure?: (err: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WatchCount {
|
||||||
|
#data = new Map<KubeObjectStore<KubeObject>, number>();
|
||||||
|
|
||||||
|
public inc(store: KubeObjectStore<KubeObject>): number {
|
||||||
|
if (!this.#data.has(store)) {
|
||||||
|
this.#data.set(store, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCount = this.#data.get(store) + 1;
|
||||||
|
|
||||||
|
logger.info(`[KUBE-WATCH-API]: inc() count for ${store.api.objectConstructor.apiBase} is now ${newCount}`);
|
||||||
|
this.#data.set(store, newCount);
|
||||||
|
|
||||||
|
return newCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dec(store: KubeObjectStore<KubeObject>): number {
|
||||||
|
if (!this.#data.has(store)) {
|
||||||
|
throw new Error(`Cannot dec count for store that has never been inc: ${store.api.objectConstructor.kind}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCount = this.#data.get(store) - 1;
|
||||||
|
|
||||||
|
if (newCount < 0) {
|
||||||
|
throw new Error(`Cannot dec count more times than it has been inc: ${store.api.objectConstructor.kind}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`[KUBE-WATCH-API]: dec() count for ${store.api.objectConstructor.apiBase} is now ${newCount}`);
|
||||||
|
this.#data.set(store, newCount);
|
||||||
|
|
||||||
|
return newCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class KubeWatchApi {
|
export class KubeWatchApi {
|
||||||
@observable context: ClusterContext = null;
|
static context: ClusterContext = null;
|
||||||
|
|
||||||
constructor() {
|
#watch = new WatchCount();
|
||||||
makeObservable(this);
|
|
||||||
autoBind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
isAllowedApi(api: KubeApi<KubeObject>): boolean {
|
private subscribeStore({ store, parent, watchChanges, namespaces, onLoadFailure }: SubscribeStoreParams): Disposer {
|
||||||
return Boolean(this.context?.cluster.isAllowedResource(api.kind));
|
if (this.#watch.inc(store) > 1) {
|
||||||
}
|
// don't load or subscribe to a store more than once
|
||||||
|
return () => this.#watch.dec(store);
|
||||||
preloadStores(stores: KubeObjectStore<KubeObject>[], { loadOnce, namespaces, onLoadFailure }: KubeWatchPreloadOptions = {}) {
|
|
||||||
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
|
|
||||||
const preloading: Promise<any>[] = [];
|
|
||||||
|
|
||||||
for (const store of stores) {
|
|
||||||
preloading.push(limitRequests(async () => {
|
|
||||||
if (store.isLoaded && loadOnce) return; // skip
|
|
||||||
|
|
||||||
return store.loadAll({ namespaces, onLoadFailure });
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
let childController = new WrappedAbortController(parent);
|
||||||
loading: Promise.allSettled(preloading),
|
const unsubscribe = disposer();
|
||||||
cancelLoading: () => limitRequests.clearQueue(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribeStores(stores: KubeObjectStore<KubeObject>[], opts: KubeWatchSubscribeStoreOptions = {}): Disposer {
|
const loadThenSubscribe = async (namespaces: string[]) => {
|
||||||
const { preload = true, waitUntilLoaded = true, loadOnce = false, onLoadFailure } = opts;
|
try {
|
||||||
const subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? [];
|
await store.loadAll({ namespaces, reqInit: { signal: childController.signal }, onLoadFailure });
|
||||||
const unsubscribeStores = disposer();
|
unsubscribe.push(store.subscribe({ onLoadFailure, abortController: childController }));
|
||||||
let isUnsubscribed = false;
|
} catch (error) {
|
||||||
|
if (!(error instanceof DOMException)) {
|
||||||
const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce, onLoadFailure });
|
this.log(Object.assign(new Error("Loading stores has failed"), { cause: error }), {
|
||||||
let preloading = preload && load();
|
meta: { store, namespaces },
|
||||||
let cancelReloading: Disposer = noop;
|
|
||||||
|
|
||||||
const subscribe = () => {
|
|
||||||
if (isUnsubscribed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsubscribeStores.push(...stores.map(store => store.subscribe({ onLoadFailure })));
|
|
||||||
};
|
|
||||||
|
|
||||||
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 => {
|
* We don't want to wait because we want to start reacting to namespace
|
||||||
preloading?.cancelLoading();
|
* selection changes ASAP
|
||||||
unsubscribeStores();
|
*/
|
||||||
preloading = load(namespaces);
|
loadThenSubscribe(namespaces).catch(noop);
|
||||||
preloading.loading.then(subscribe);
|
|
||||||
}, {
|
const cancelReloading = watchChanges
|
||||||
equals: comparer.shallow,
|
? reaction(
|
||||||
});
|
// Note: must slice because reaction won't fire if it isn't there
|
||||||
}
|
() => [KubeWatchApi.context.contextNamespaces.slice(), KubeWatchApi.context.hasSelectedAll] as const,
|
||||||
|
([namespaces, curSelectedAll], [prevNamespaces, prevSelectedAll]) => {
|
||||||
|
if (curSelectedAll && prevSelectedAll) {
|
||||||
|
const action = namespaces.length > prevNamespaces.length ? "created" : "deleted";
|
||||||
|
|
||||||
|
return console.debug(`[KUBE-WATCH-API]: Not changing watch for ${store.api.apiBase} because a new namespace was ${action} but all namespaces are selected`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[KUBE-WATCH-API]: changing watch ${store.api.apiBase}`, namespaces);
|
||||||
|
childController.abort();
|
||||||
|
unsubscribe();
|
||||||
|
childController = new WrappedAbortController(parent);
|
||||||
|
loadThenSubscribe(namespaces).catch(noop);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
equals: comparer.shallow,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: noop; // don't watch namespaces if namespaces were provided
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (this.#watch.dec(store) === 0) {
|
||||||
|
// only stop the subcribe if this is the last one
|
||||||
|
cancelReloading();
|
||||||
|
childController.abort();
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeStores(stores: KubeObjectStore<KubeObject>[], { namespaces, onLoadFailure }: KubeWatchSubscribeStoreOptions = {}): Disposer {
|
||||||
|
const parent = new AbortController();
|
||||||
|
const unsubscribe = disposer(
|
||||||
|
...stores.map(store => this.subscribeStore({
|
||||||
|
store,
|
||||||
|
parent,
|
||||||
|
watchChanges: !namespaces && store.api.isNamespaced,
|
||||||
|
namespaces: namespaces ?? KubeWatchApi.context?.contextNamespaces ?? [],
|
||||||
|
onLoadFailure,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
// unsubscribe
|
// unsubscribe
|
||||||
return () => {
|
return once(() => {
|
||||||
if (isUnsubscribed) return;
|
parent.abort();
|
||||||
isUnsubscribed = true;
|
unsubscribe();
|
||||||
cancelReloading();
|
});
|
||||||
preloading?.cancelLoading();
|
|
||||||
unsubscribeStores();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected log({ message, cssStyle = "", meta = {}}: IKubeWatchLog) {
|
protected log(message: any, meta: any) {
|
||||||
if (isProduction && !isDebugging) {
|
const log = message instanceof Error
|
||||||
return;
|
? console.error
|
||||||
}
|
: console.debug;
|
||||||
|
|
||||||
const logInfo = [`%c[KUBE-WATCH-API]:`, `font-weight: bold; ${cssStyle}`, message].flat().map(String);
|
log("[KUBE-WATCH-API]:", message, {
|
||||||
const logMeta = {
|
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
...meta,
|
...meta,
|
||||||
};
|
});
|
||||||
|
|
||||||
if (message instanceof Error) {
|
|
||||||
console.error(...logInfo, logMeta);
|
|
||||||
} else {
|
|
||||||
console.info(...logInfo, logMeta);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -142,9 +142,9 @@ describe("kubeconfig manager tests", () => {
|
|||||||
const configPath = await kubeConfManager.getPath();
|
const configPath = await kubeConfManager.getPath();
|
||||||
|
|
||||||
expect(await fse.pathExists(configPath)).toBe(true);
|
expect(await fse.pathExists(configPath)).toBe(true);
|
||||||
await kubeConfManager.unlink();
|
await kubeConfManager.clear();
|
||||||
expect(await fse.pathExists(configPath)).toBe(false);
|
expect(await fse.pathExists(configPath)).toBe(false);
|
||||||
await kubeConfManager.unlink(); // doesn't throw
|
await kubeConfManager.clear(); // doesn't throw
|
||||||
expect(async () => {
|
expect(async () => {
|
||||||
await kubeConfManager.getPath();
|
await kubeConfManager.getPath();
|
||||||
}).rejects.toThrow("already unlinked");
|
}).rejects.toThrow("already unlinked");
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import { DetectorRegistry } from "./cluster-detectors/detector-registry";
|
|||||||
import plimit from "p-limit";
|
import plimit from "p-limit";
|
||||||
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../common/cluster-types";
|
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../common/cluster-types";
|
||||||
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types";
|
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types";
|
||||||
import { storedKubeConfigFolder, toJS } from "../common/utils";
|
import { disposer, storedKubeConfigFolder, toJS } from "../common/utils";
|
||||||
import type { Response } from "request";
|
import type { Response } from "request";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,8 +52,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
public contextHandler: ContextHandler;
|
public contextHandler: ContextHandler;
|
||||||
protected kubeconfigManager: KubeconfigManager;
|
protected proxyKubeconfigManager: KubeconfigManager;
|
||||||
protected eventDisposers: Function[] = [];
|
protected eventsDisposer = disposer();
|
||||||
protected activated = false;
|
protected activated = false;
|
||||||
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map();
|
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map();
|
||||||
|
|
||||||
@ -218,9 +218,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@computed get defaultNamespace(): string {
|
@computed get defaultNamespace(): string {
|
||||||
const { defaultNamespace } = this.preferences;
|
return this.preferences.defaultNamespace;
|
||||||
|
|
||||||
return defaultNamespace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(model: ClusterModel) {
|
constructor(model: ClusterModel) {
|
||||||
@ -240,7 +238,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
// for the time being, until renderer gets its own cluster type
|
// for the time being, until renderer gets its own cluster type
|
||||||
this.contextHandler = new ContextHandler(this);
|
this.contextHandler = new ContextHandler(this);
|
||||||
this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler);
|
this.proxyKubeconfigManager = new KubeconfigManager(this, this.contextHandler);
|
||||||
|
|
||||||
logger.debug(`[CLUSTER]: Cluster init success`, {
|
logger.debug(`[CLUSTER]: Cluster init success`, {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
@ -297,40 +295,31 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
|
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
|
||||||
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes
|
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes
|
||||||
|
|
||||||
if (ipcMain) {
|
this.eventsDisposer.push(
|
||||||
this.eventDisposers.push(
|
reaction(() => this.getState(), state => this.pushState(state)),
|
||||||
reaction(() => this.getState(), () => this.pushState()),
|
reaction(
|
||||||
reaction(() => this.prometheusPreferences, (prefs) => this.contextHandler.setupPrometheus(prefs), { equals: comparer.structural }),
|
() => this.prometheusPreferences,
|
||||||
() => {
|
prefs => this.contextHandler.setupPrometheus(prefs),
|
||||||
clearInterval(refreshTimer);
|
{ equals: comparer.structural },
|
||||||
clearInterval(refreshMetadataTimer);
|
),
|
||||||
},
|
() => clearInterval(refreshTimer),
|
||||||
reaction(() => this.defaultNamespace, () => this.recreateProxyKubeconfig()),
|
() => clearInterval(refreshMetadataTimer),
|
||||||
);
|
reaction(() => this.defaultNamespace, () => this.recreateProxyKubeconfig()),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
async recreateProxyKubeconfig() {
|
protected async recreateProxyKubeconfig() {
|
||||||
logger.info("Recreate proxy kubeconfig");
|
logger.info("[CLUSTER]: Recreating proxy kubeconfig");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.kubeconfigManager.clear();
|
await this.proxyKubeconfigManager.clear();
|
||||||
} catch {
|
await this.getProxyKubeconfig();
|
||||||
// do nothing
|
} catch (error) {
|
||||||
|
logger.error(`[CLUSTER]: failed to recreate proxy kubeconfig`, error);
|
||||||
}
|
}
|
||||||
this.getProxyKubeconfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* internal
|
|
||||||
*/
|
|
||||||
protected unbindEvents() {
|
|
||||||
logger.info(`[CLUSTER]: unbind events`, this.getMeta());
|
|
||||||
this.eventDisposers.forEach(dispose => dispose());
|
|
||||||
this.eventDisposers.length = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -345,7 +334,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
|
|
||||||
logger.info(`[CLUSTER]: activate`, this.getMeta());
|
logger.info(`[CLUSTER]: activate`, this.getMeta());
|
||||||
|
|
||||||
if (!this.eventDisposers.length) {
|
if (!this.eventsDisposer.length) {
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,7 +384,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action disconnect() {
|
@action disconnect() {
|
||||||
this.unbindEvents();
|
logger.info(`[CLUSTER]: disconnecting`, { id: this.id });
|
||||||
|
this.eventsDisposer();
|
||||||
this.contextHandler?.stopServer();
|
this.contextHandler?.stopServer();
|
||||||
this.disconnected = true;
|
this.disconnected = true;
|
||||||
this.online = false;
|
this.online = false;
|
||||||
@ -405,7 +395,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
this.allowedNamespaces = [];
|
this.allowedNamespaces = [];
|
||||||
this.resourceAccessStatuses.clear();
|
this.resourceAccessStatuses.clear();
|
||||||
this.pushState();
|
this.pushState();
|
||||||
logger.info(`[CLUSTER]: disconnect`, this.getMeta());
|
logger.info(`[CLUSTER]: disconnected`, { id: this.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -481,7 +471,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
async getProxyKubeconfigPath(): Promise<string> {
|
async getProxyKubeconfigPath(): Promise<string> {
|
||||||
return this.kubeconfigManager.getPath();
|
return this.proxyKubeconfigManager.getPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getConnectionStatus(): Promise<ClusterStatus> {
|
protected async getConnectionStatus(): Promise<ClusterStatus> {
|
||||||
|
|||||||
@ -34,12 +34,19 @@ export interface PrometheusDetails {
|
|||||||
provider: PrometheusProvider;
|
provider: PrometheusProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PrometheusServicePreferences {
|
||||||
|
namespace: string;
|
||||||
|
service: string;
|
||||||
|
port: number;
|
||||||
|
prefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class ContextHandler {
|
export class ContextHandler {
|
||||||
public clusterUrl: UrlWithStringQuery;
|
public clusterUrl: UrlWithStringQuery;
|
||||||
protected kubeAuthProxy?: KubeAuthProxy;
|
protected kubeAuthProxy?: KubeAuthProxy;
|
||||||
protected apiTarget?: httpProxy.ServerOptions;
|
protected apiTarget?: httpProxy.ServerOptions;
|
||||||
protected prometheusProvider?: string;
|
protected prometheusProvider?: string;
|
||||||
protected prometheusPath: string | null;
|
protected prometheus?: PrometheusServicePreferences;
|
||||||
|
|
||||||
constructor(protected cluster: Cluster) {
|
constructor(protected cluster: Cluster) {
|
||||||
this.clusterUrl = url.parse(cluster.apiUrl);
|
this.clusterUrl = url.parse(cluster.apiUrl);
|
||||||
@ -48,13 +55,7 @@ export class ContextHandler {
|
|||||||
|
|
||||||
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
|
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
|
||||||
this.prometheusProvider = preferences.prometheusProvider?.type;
|
this.prometheusProvider = preferences.prometheusProvider?.type;
|
||||||
this.prometheusPath = null;
|
this.prometheus = preferences.prometheus || null;
|
||||||
|
|
||||||
if (preferences.prometheus) {
|
|
||||||
const { namespace, service, port } = preferences.prometheus;
|
|
||||||
|
|
||||||
this.prometheusPath = `${namespace}/services/${service}:${port}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPrometheusDetails(): Promise<PrometheusDetails> {
|
public async getPrometheusDetails(): Promise<PrometheusDetails> {
|
||||||
@ -66,7 +67,7 @@ export class ContextHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected ensurePrometheusPath({ service, namespace, port }: PrometheusService): string {
|
protected ensurePrometheusPath({ service, namespace, port }: PrometheusService): string {
|
||||||
return this.prometheusPath ||= `${namespace}/services/${service}:${port}`;
|
return `${namespace}/services/${service}:${port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ensurePrometheusProvider(service: PrometheusService): PrometheusProvider {
|
protected ensurePrometheusProvider(service: PrometheusService): PrometheusProvider {
|
||||||
@ -90,6 +91,15 @@ export class ContextHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async getPrometheusService(): Promise<PrometheusService> {
|
protected async getPrometheusService(): Promise<PrometheusService> {
|
||||||
|
if (this.prometheus !== null && this.prometheusProvider !== null) {
|
||||||
|
return {
|
||||||
|
id: this.prometheusProvider,
|
||||||
|
namespace: this.prometheus.namespace,
|
||||||
|
service: this.prometheus.service,
|
||||||
|
port: this.prometheus.port,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const providers = this.listPotentialProviders();
|
const providers = this.listPotentialProviders();
|
||||||
const proxyConfig = await this.cluster.getProxyKubeconfig();
|
const proxyConfig = await this.cluster.getProxyKubeconfig();
|
||||||
const apiClient = proxyConfig.makeApiClient(CoreV1Api);
|
const apiClient = proxyConfig.makeApiClient(CoreV1Api);
|
||||||
|
|||||||
@ -32,30 +32,21 @@ import { makeObservable, observable, when } from "mobx";
|
|||||||
const startingServeRegex = /^starting to serve on (?<address>.+)/i;
|
const startingServeRegex = /^starting to serve on (?<address>.+)/i;
|
||||||
|
|
||||||
export class KubeAuthProxy {
|
export class KubeAuthProxy {
|
||||||
public readonly apiPrefix: string;
|
public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`;
|
||||||
|
|
||||||
public get port(): number {
|
public get port(): number {
|
||||||
return this._port;
|
return this._port;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _port?: number;
|
protected _port: number;
|
||||||
protected cluster: Cluster;
|
protected proxyProcess?: ChildProcess;
|
||||||
protected env: NodeJS.ProcessEnv = null;
|
protected readonly acceptHosts: string;
|
||||||
protected proxyProcess: ChildProcess;
|
@observable protected ready = false;
|
||||||
protected kubectl: Kubectl;
|
|
||||||
@observable protected ready: boolean;
|
|
||||||
|
|
||||||
constructor(cluster: Cluster, env: NodeJS.ProcessEnv) {
|
constructor(protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
this.ready = false;
|
|
||||||
this.env = env;
|
|
||||||
this.cluster = cluster;
|
|
||||||
this.kubectl = Kubectl.bundled();
|
|
||||||
this.apiPrefix = `/${randomBytes(8).toString("hex")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
get acceptHosts() {
|
this.acceptHosts = url.parse(this.cluster.apiUrl).hostname;
|
||||||
return url.parse(this.cluster.apiUrl).hostname;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get whenReady() {
|
get whenReady() {
|
||||||
@ -67,7 +58,7 @@ export class KubeAuthProxy {
|
|||||||
return this.whenReady;
|
return this.whenReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyBin = await this.kubectl.getPath();
|
const proxyBin = await Kubectl.bundled().getPath();
|
||||||
const args = [
|
const args = [
|
||||||
"proxy",
|
"proxy",
|
||||||
"-p", "0",
|
"-p", "0",
|
||||||
|
|||||||
@ -30,57 +30,60 @@ import { LensProxy } from "./lens-proxy";
|
|||||||
import { AppPaths } from "../common/app-paths";
|
import { AppPaths } from "../common/app-paths";
|
||||||
|
|
||||||
export class KubeconfigManager {
|
export class KubeconfigManager {
|
||||||
protected configDir = AppPaths.get("temp");
|
/**
|
||||||
protected tempFile: string = null;
|
* The path to the temp config file
|
||||||
|
*
|
||||||
|
* - if `string` then path
|
||||||
|
* - if `null` then not yet created
|
||||||
|
* - if `undefined` then unlinked by calling `clear()`
|
||||||
|
*/
|
||||||
|
protected tempFilePath: string | null | undefined = null;
|
||||||
|
|
||||||
constructor(protected cluster: Cluster, protected contextHandler: ContextHandler) { }
|
constructor(protected cluster: Cluster, protected contextHandler: ContextHandler) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns The path to the temporary kubeconfig
|
||||||
|
*/
|
||||||
async getPath(): Promise<string> {
|
async getPath(): Promise<string> {
|
||||||
if (this.tempFile === undefined) {
|
if (this.tempFilePath === undefined) {
|
||||||
throw new Error("kubeconfig is already unlinked");
|
throw new Error("kubeconfig is already unlinked");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.tempFile) {
|
if (this.tempFilePath === null || !(await fs.pathExists(this.tempFilePath))) {
|
||||||
await this.init();
|
await this.ensureFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
// create proxy kubeconfig if it is removed without unlink called
|
return this.tempFilePath;
|
||||||
if (!(await fs.pathExists(this.tempFile))) {
|
}
|
||||||
try {
|
|
||||||
this.tempFile = await this.createProxyKubeconfig();
|
/**
|
||||||
} catch (err) {
|
* Deletes the temporary kubeconfig file
|
||||||
logger.error(`[KUBECONFIG-MANAGER]: Failed to created temp config for auth-proxy`, { err });
|
*/
|
||||||
|
async clear(): Promise<void> {
|
||||||
|
if (!this.tempFilePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[KUBECONFIG-MANAGER]: Deleting temporary kubeconfig: ${this.tempFilePath}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.unlink(this.tempFilePath);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "ENOENT") {
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
this.tempFilePath = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.tempFile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear() {
|
protected async ensureFile() {
|
||||||
if (!this.tempFile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`[KUBECONFIG-MANAGER]: Deleting temporary kubeconfig: ${this.tempFile}`);
|
|
||||||
await fs.unlink(this.tempFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
async unlink() {
|
|
||||||
if (!this.tempFile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`[KUBECONFIG-MANAGER]: Deleting temporary kubeconfig: ${this.tempFile}`);
|
|
||||||
await fs.unlink(this.tempFile);
|
|
||||||
this.tempFile = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async init() {
|
|
||||||
try {
|
try {
|
||||||
await this.contextHandler.ensureServer();
|
await this.contextHandler.ensureServer();
|
||||||
this.tempFile = await this.createProxyKubeconfig();
|
this.tempFilePath = await this.createProxyKubeconfig();
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
logger.error(`[KUBECONFIG-MANAGER]: Failed to created temp config for auth-proxy`, err);
|
throw Object.assign(new Error("Failed to creat temp config for auth-proxy"), { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,9 +96,9 @@ export class KubeconfigManager {
|
|||||||
* This way any user of the config does not need to know anything about the auth etc. details.
|
* This way any user of the config does not need to know anything about the auth etc. details.
|
||||||
*/
|
*/
|
||||||
protected async createProxyKubeconfig(): Promise<string> {
|
protected async createProxyKubeconfig(): Promise<string> {
|
||||||
const { configDir, cluster } = this;
|
const { cluster } = this;
|
||||||
const { contextName, id } = cluster;
|
const { contextName, id } = cluster;
|
||||||
const tempFile = path.join(configDir, `kubeconfig-${id}`);
|
const tempFile = path.join(AppPaths.get("temp"), `kubeconfig-${id}`);
|
||||||
const kubeConfig = await cluster.getKubeconfig();
|
const kubeConfig = await cluster.getKubeconfig();
|
||||||
const proxyConfig: Partial<KubeConfig> = {
|
const proxyConfig: Partial<KubeConfig> = {
|
||||||
currentContext: contextName,
|
currentContext: contextName,
|
||||||
|
|||||||
@ -42,11 +42,11 @@ import whatInput from "what-input";
|
|||||||
import { clusterSetFrameIdHandler } from "../common/cluster-ipc";
|
import { clusterSetFrameIdHandler } from "../common/cluster-ipc";
|
||||||
import { ClusterPageMenuRegistration, ClusterPageMenuRegistry } from "../extensions/registries";
|
import { ClusterPageMenuRegistration, ClusterPageMenuRegistry } from "../extensions/registries";
|
||||||
import { StatefulSetScaleDialog } from "./components/+workloads-statefulsets/statefulset-scale-dialog";
|
import { StatefulSetScaleDialog } from "./components/+workloads-statefulsets/statefulset-scale-dialog";
|
||||||
import { kubeWatchApi } from "../common/k8s-api/kube-watch-api";
|
import { KubeWatchApi, kubeWatchApi } from "../common/k8s-api/kube-watch-api";
|
||||||
import { ReplicaSetScaleDialog } from "./components/+workloads-replicasets/replicaset-scale-dialog";
|
import { ReplicaSetScaleDialog } from "./components/+workloads-replicasets/replicaset-scale-dialog";
|
||||||
import { CommandContainer } from "./components/command-palette/command-container";
|
import { CommandContainer } from "./components/command-palette/command-container";
|
||||||
import { KubeObjectStore } from "../common/k8s-api/kube-object.store";
|
import { KubeObjectStore } from "../common/k8s-api/kube-object.store";
|
||||||
import { clusterContext } from "./components/context";
|
import { FrameContext } from "./components/context";
|
||||||
import * as routes from "../common/routes";
|
import * as routes from "../common/routes";
|
||||||
import { TabLayout, TabLayoutRoute } from "./components/layout/tab-layout";
|
import { TabLayout, TabLayoutRoute } from "./components/layout/tab-layout";
|
||||||
import { ErrorBoundary } from "./components/error-boundary";
|
import { ErrorBoundary } from "./components/error-boundary";
|
||||||
@ -73,6 +73,8 @@ import { watchHistoryState } from "./remote-helpers/history-updater";
|
|||||||
import { unmountComponentAtNode } from "react-dom";
|
import { unmountComponentAtNode } from "react-dom";
|
||||||
import { PortForwardDialog } from "./port-forward";
|
import { PortForwardDialog } from "./port-forward";
|
||||||
import { DeleteClusterDialog } from "./components/delete-cluster-dialog";
|
import { DeleteClusterDialog } from "./components/delete-cluster-dialog";
|
||||||
|
import { WorkloadsOverview } from "./components/+workloads-overview/overview";
|
||||||
|
import { KubeObjectListLayout } from "./components/kube-object-list-layout";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterFrame extends React.Component {
|
export class ClusterFrame extends React.Component {
|
||||||
@ -91,10 +93,12 @@ export class ClusterFrame extends React.Component {
|
|||||||
|
|
||||||
ClusterFrame.clusterId = getHostedClusterId();
|
ClusterFrame.clusterId = getHostedClusterId();
|
||||||
|
|
||||||
|
const cluster = ClusterStore.getInstance().getById(ClusterFrame.clusterId);
|
||||||
|
|
||||||
logger.info(`${ClusterFrame.logPrefix} Init dashboard, clusterId=${ClusterFrame.clusterId}, frameId=${frameId}`);
|
logger.info(`${ClusterFrame.logPrefix} Init dashboard, clusterId=${ClusterFrame.clusterId}, frameId=${frameId}`);
|
||||||
await Terminal.preloadFonts();
|
await Terminal.preloadFonts();
|
||||||
await requestMain(clusterSetFrameIdHandler, ClusterFrame.clusterId);
|
await requestMain(clusterSetFrameIdHandler, ClusterFrame.clusterId);
|
||||||
await ClusterStore.getInstance().getById(ClusterFrame.clusterId).whenReady; // cluster.activate() is done at this point
|
await cluster.whenReady; // cluster.activate() is done at this point
|
||||||
|
|
||||||
catalogEntityRegistry.activeEntity = ClusterFrame.clusterId;
|
catalogEntityRegistry.activeEntity = ClusterFrame.clusterId;
|
||||||
|
|
||||||
@ -120,16 +124,21 @@ export class ClusterFrame extends React.Component {
|
|||||||
|
|
||||||
whatInput.ask(); // Start to monitor user input device
|
whatInput.ask(); // Start to monitor user input device
|
||||||
|
|
||||||
|
const clusterContext = new FrameContext(cluster);
|
||||||
|
|
||||||
// Setup hosted cluster context
|
// Setup hosted cluster context
|
||||||
KubeObjectStore.defaultContext.set(clusterContext);
|
KubeObjectStore.defaultContext.set(clusterContext);
|
||||||
kubeWatchApi.context = clusterContext;
|
WorkloadsOverview.clusterContext
|
||||||
|
= KubeObjectListLayout.clusterContext
|
||||||
|
= KubeWatchApi.context
|
||||||
|
= clusterContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
kubeWatchApi.subscribeStores([namespaceStore], {
|
kubeWatchApi.subscribeStores([
|
||||||
preload: true,
|
namespaceStore,
|
||||||
}),
|
]),
|
||||||
|
|
||||||
watchHistoryState(),
|
watchHistoryState(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -55,9 +55,11 @@ export class ClusterOverview extends React.Component {
|
|||||||
this.metricPoller.start(true);
|
this.metricPoller.start(true);
|
||||||
|
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
kubeWatchApi.subscribeStores([podsStore, eventStore, nodesStore], {
|
kubeWatchApi.subscribeStores([
|
||||||
preload: true,
|
podsStore,
|
||||||
}),
|
eventStore,
|
||||||
|
nodesStore,
|
||||||
|
]),
|
||||||
reaction(
|
reaction(
|
||||||
() => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
|
() => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
|
||||||
() => this.metricPoller.restart(true),
|
() => this.metricPoller.restart(true),
|
||||||
|
|||||||
@ -22,13 +22,14 @@
|
|||||||
import "./kube-event-details.scss";
|
import "./kube-event-details.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { LocaleDate } from "../locale-date";
|
import { LocaleDate } from "../locale-date";
|
||||||
import { eventStore } from "./event.store";
|
import { eventStore } from "./event.store";
|
||||||
import logger from "../../../common/logger";
|
import logger from "../../../common/logger";
|
||||||
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
|
|
||||||
export interface KubeEventDetailsProps {
|
export interface KubeEventDetailsProps {
|
||||||
object: KubeObject;
|
object: KubeObject;
|
||||||
@ -36,8 +37,12 @@ export interface KubeEventDetailsProps {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
eventStore.reloadAll();
|
disposeOnUnmount(this, [
|
||||||
|
kubeWatchApi.subscribeStores([
|
||||||
|
eventStore,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
|||||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||||
import { getDetailsUrl } from "../kube-detail-params";
|
import { getDetailsUrl } from "../kube-detail-params";
|
||||||
import logger from "../../../common/logger";
|
import logger from "../../../common/logger";
|
||||||
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<Namespace> {
|
interface Props extends KubeObjectDetailsProps<Namespace> {
|
||||||
}
|
}
|
||||||
@ -52,14 +53,16 @@ export class NamespaceDetails extends React.Component<Props> {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@disposeOnUnmount
|
|
||||||
clean = reaction(() => this.props.object, () => {
|
|
||||||
this.metrics = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
resourceQuotaStore.reloadAll();
|
disposeOnUnmount(this, [
|
||||||
limitRangeStore.reloadAll();
|
reaction(() => this.props.object, () => {
|
||||||
|
this.metrics = null;
|
||||||
|
}),
|
||||||
|
kubeWatchApi.subscribeStores([
|
||||||
|
resourceQuotaStore,
|
||||||
|
limitRangeStore,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get quotas() {
|
@computed get quotas() {
|
||||||
|
|||||||
@ -23,12 +23,11 @@ import "./namespace-select.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { computed, makeObservable } from "mobx";
|
import { computed, makeObservable } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Select, SelectOption, SelectProps } from "../select";
|
import { Select, SelectOption, SelectProps } from "../select";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { namespaceStore } from "./namespace.store";
|
import { namespaceStore } from "./namespace.store";
|
||||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
|
||||||
|
|
||||||
interface Props extends SelectProps {
|
interface Props extends SelectProps {
|
||||||
showIcons?: boolean;
|
showIcons?: boolean;
|
||||||
@ -50,14 +49,7 @@ export class NamespaceSelect extends React.Component<Props> {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
// No subscribe here because the subscribe is in <App /> (the cluster frame root component)
|
||||||
disposeOnUnmount(this, [
|
|
||||||
kubeWatchApi.subscribeStores([namespaceStore], {
|
|
||||||
preload: true,
|
|
||||||
loadOnce: true, // skip reloading namespaces on every render / page visit
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed.struct get options(): SelectOption[] {
|
@computed.struct get options(): SelectOption[] {
|
||||||
const { customizeOptions, showAllNamespacesOption, sort } = this.props;
|
const { customizeOptions, showAllNamespacesOption, sort } = this.props;
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { action, comparer, computed, IReactionDisposer, IReactionOptions, makeObservable, reaction } from "mobx";
|
import { action, comparer, computed, IReactionDisposer, makeObservable, reaction } from "mobx";
|
||||||
import { autoBind, createStorage, noop, ToggleSet } from "../../utils";
|
import { autoBind, createStorage, noop, ToggleSet } from "../../utils";
|
||||||
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../../common/k8s-api/kube-object.store";
|
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../../common/k8s-api/kube-object.store";
|
||||||
import { Namespace, namespacesApi } from "../../../common/k8s-api/endpoints/namespaces.api";
|
import { Namespace, namespacesApi } from "../../../common/k8s-api/endpoints/namespaces.api";
|
||||||
@ -45,10 +45,10 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
this.autoLoadAllowedNamespaces();
|
this.autoLoadAllowedNamespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onContextChange(callback: (namespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
|
public onContextChange(callback: (namespaces: string[]) => void, opts: { fireImmediately?: boolean } = {}): IReactionDisposer {
|
||||||
return reaction(() => Array.from(this.contextNamespaces), callback, {
|
return reaction(() => Array.from(this.contextNamespaces), callback, {
|
||||||
|
fireImmediately: opts.fireImmediately,
|
||||||
equals: comparer.shallow,
|
equals: comparer.shallow,
|
||||||
...opts,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
return super.subscribe();
|
return super.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadItems(params: KubeObjectStoreLoadingParams<Namespace>): Promise<Namespace[]> {
|
protected async loadItems(params: KubeObjectStoreLoadingParams): Promise<Namespace[]> {
|
||||||
const { allowedNamespaces } = this;
|
const { allowedNamespaces } = this;
|
||||||
|
|
||||||
let namespaces = await super.loadItems(params).catch(() => []);
|
let namespaces = await super.loadItems(params).catch(() => []);
|
||||||
@ -205,7 +205,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
* explicitly deselected.
|
* explicitly deselected.
|
||||||
* @param namespace The name of a namespace
|
* @param namespace The name of a namespace
|
||||||
*/
|
*/
|
||||||
toggleSingle(namespace: string){
|
toggleSingle(namespace: string) {
|
||||||
const nextState = new ToggleSet(this.contextNamespaces);
|
const nextState = new ToggleSet(this.contextNamespaces);
|
||||||
|
|
||||||
nextState.toggle(namespace);
|
nextState.toggle(namespace);
|
||||||
|
|||||||
@ -49,10 +49,13 @@ export class IngressDetails extends React.Component<Props> {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@disposeOnUnmount
|
componentDidMount() {
|
||||||
clean = reaction(() => this.props.object, () => {
|
disposeOnUnmount(this, [
|
||||||
this.metrics = null;
|
reaction(() => this.props.object, () => {
|
||||||
});
|
this.metrics = null;
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
async loadMetrics() {
|
async loadMetrics() {
|
||||||
|
|||||||
@ -44,8 +44,9 @@ export class ServiceDetails extends React.Component<Props> {
|
|||||||
const { object: service } = this.props;
|
const { object: service } = this.props;
|
||||||
|
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
kubeWatchApi.subscribeStores([endpointStore], {
|
kubeWatchApi.subscribeStores([
|
||||||
preload: true,
|
endpointStore,
|
||||||
|
], {
|
||||||
namespaces: [service.getNs()],
|
namespaces: [service.getNs()],
|
||||||
}),
|
}),
|
||||||
portForwardStore.watch(),
|
portForwardStore.watch(),
|
||||||
|
|||||||
@ -41,6 +41,7 @@ import { NodeDetailsResources } from "./node-details-resources";
|
|||||||
import { DrawerTitle } from "../drawer/drawer-title";
|
import { DrawerTitle } from "../drawer/drawer-title";
|
||||||
import { boundMethod } from "../../utils";
|
import { boundMethod } from "../../utils";
|
||||||
import logger from "../../../common/logger";
|
import logger from "../../../common/logger";
|
||||||
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<Node> {
|
interface Props extends KubeObjectDetailsProps<Node> {
|
||||||
}
|
}
|
||||||
@ -54,13 +55,15 @@ export class NodeDetails extends React.Component<Props> {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@disposeOnUnmount
|
componentDidMount() {
|
||||||
clean = reaction(() => this.props.object.getName(), () => {
|
disposeOnUnmount(this, [
|
||||||
this.metrics = null;
|
reaction(() => this.props.object.getName(), () => {
|
||||||
});
|
this.metrics = null;
|
||||||
|
}),
|
||||||
async componentDidMount() {
|
kubeWatchApi.subscribeStores([
|
||||||
podsStore.reloadAll();
|
podsStore,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import React from "react";
|
|||||||
import startCase from "lodash/startCase";
|
import startCase from "lodash/startCase";
|
||||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||||
import { StorageClass } from "../../../common/k8s-api/endpoints";
|
import { StorageClass } from "../../../common/k8s-api/endpoints";
|
||||||
import { KubeObjectMeta } from "../kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object-meta";
|
||||||
@ -33,14 +33,19 @@ import { storageClassStore } from "./storage-class.store";
|
|||||||
import { VolumeDetailsList } from "../+storage-volumes/volume-details-list";
|
import { VolumeDetailsList } from "../+storage-volumes/volume-details-list";
|
||||||
import { volumesStore } from "../+storage-volumes/volumes.store";
|
import { volumesStore } from "../+storage-volumes/volumes.store";
|
||||||
import logger from "../../../common/logger";
|
import logger from "../../../common/logger";
|
||||||
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<StorageClass> {
|
interface Props extends KubeObjectDetailsProps<StorageClass> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class StorageClassDetails extends React.Component<Props> {
|
export class StorageClassDetails extends React.Component<Props> {
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
volumesStore.reloadAll();
|
disposeOnUnmount(this, [
|
||||||
|
kubeWatchApi.subscribeStores([
|
||||||
|
volumesStore,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -51,10 +51,13 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@disposeOnUnmount
|
componentDidMount() {
|
||||||
clean = reaction(() => this.props.object, () => {
|
disposeOnUnmount(this, [
|
||||||
this.metrics = null;
|
reaction(() => this.props.object, () => {
|
||||||
});
|
this.metrics = null;
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
async loadMetrics() {
|
async loadMetrics() {
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import "./cronjob-details.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import kebabCase from "lodash/kebabCase";
|
import kebabCase from "lodash/kebabCase";
|
||||||
import { observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||||
import { Badge } from "../badge/badge";
|
import { Badge } from "../badge/badge";
|
||||||
import { jobStore } from "../+workloads-jobs/job.store";
|
import { jobStore } from "../+workloads-jobs/job.store";
|
||||||
@ -34,14 +34,19 @@ import { getDetailsUrl } from "../kube-detail-params";
|
|||||||
import { CronJob, Job } from "../../../common/k8s-api/endpoints";
|
import { CronJob, Job } from "../../../common/k8s-api/endpoints";
|
||||||
import { KubeObjectMeta } from "../kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object-meta";
|
||||||
import logger from "../../../common/logger";
|
import logger from "../../../common/logger";
|
||||||
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<CronJob> {
|
interface Props extends KubeObjectDetailsProps<CronJob> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class CronJobDetails extends React.Component<Props> {
|
export class CronJobDetails extends React.Component<Props> {
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
jobStore.reloadAll();
|
disposeOnUnmount(this, [
|
||||||
|
kubeWatchApi.subscribeStores([
|
||||||
|
jobStore,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -41,6 +41,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
|||||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||||
import { boundMethod } from "../../utils";
|
import { boundMethod } from "../../utils";
|
||||||
import logger from "../../../common/logger";
|
import logger from "../../../common/logger";
|
||||||
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<DaemonSet> {
|
interface Props extends KubeObjectDetailsProps<DaemonSet> {
|
||||||
}
|
}
|
||||||
@ -54,13 +55,15 @@ export class DaemonSetDetails extends React.Component<Props> {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@disposeOnUnmount
|
|
||||||
clean = reaction(() => this.props.object, () => {
|
|
||||||
this.metrics = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
podsStore.reloadAll();
|
disposeOnUnmount(this, [
|
||||||
|
reaction(() => this.props.object, () => {
|
||||||
|
this.metrics = null;
|
||||||
|
}),
|
||||||
|
kubeWatchApi.subscribeStores([
|
||||||
|
podsStore,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
|
|||||||
@ -43,6 +43,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
|||||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||||
import { boundMethod } from "../../utils";
|
import { boundMethod } from "../../utils";
|
||||||
import logger from "../../../common/logger";
|
import logger from "../../../common/logger";
|
||||||
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<Deployment> {
|
interface Props extends KubeObjectDetailsProps<Deployment> {
|
||||||
}
|
}
|
||||||
@ -56,14 +57,16 @@ export class DeploymentDetails extends React.Component<Props> {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@disposeOnUnmount
|
|
||||||
clean = reaction(() => this.props.object, () => {
|
|
||||||
this.metrics = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
podsStore.reloadAll();
|
disposeOnUnmount(this, [
|
||||||
replicaSetStore.reloadAll();
|
reaction(() => this.props.object, () => {
|
||||||
|
this.metrics = null;
|
||||||
|
}),
|
||||||
|
kubeWatchApi.subscribeStores([
|
||||||
|
podsStore,
|
||||||
|
replicaSetStore,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import "./job-details.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import kebabCase from "lodash/kebabCase";
|
import kebabCase from "lodash/kebabCase";
|
||||||
import { observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { DrawerItem } from "../drawer";
|
import { DrawerItem } from "../drawer";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { PodDetailsStatuses } from "../+workloads-pods/pod-details-statuses";
|
import { PodDetailsStatuses } from "../+workloads-pods/pod-details-statuses";
|
||||||
@ -36,7 +36,7 @@ import type { KubeObjectDetailsProps } from "../kube-object-details";
|
|||||||
import { getMetricsForJobs, IPodMetrics, Job } from "../../../common/k8s-api/endpoints";
|
import { getMetricsForJobs, IPodMetrics, Job } from "../../../common/k8s-api/endpoints";
|
||||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||||
import { KubeObjectMeta } from "../kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object-meta";
|
||||||
import { makeObservable, observable } from "mobx";
|
import { makeObservable, observable, reaction } from "mobx";
|
||||||
import { podMetricTabs, PodCharts } from "../+workloads-pods/pod-charts";
|
import { podMetricTabs, PodCharts } from "../+workloads-pods/pod-charts";
|
||||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||||
@ -45,6 +45,7 @@ import { boundMethod } from "autobind-decorator";
|
|||||||
import { getDetailsUrl } from "../kube-detail-params";
|
import { getDetailsUrl } from "../kube-detail-params";
|
||||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||||
import logger from "../../../common/logger";
|
import logger from "../../../common/logger";
|
||||||
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<Job> {
|
interface Props extends KubeObjectDetailsProps<Job> {
|
||||||
}
|
}
|
||||||
@ -58,8 +59,15 @@ export class JobDetails extends React.Component<Props> {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
podsStore.reloadAll();
|
disposeOnUnmount(this, [
|
||||||
|
reaction(() => this.props.object, () => {
|
||||||
|
this.metrics = null;
|
||||||
|
}),
|
||||||
|
kubeWatchApi.subscribeStores([
|
||||||
|
podsStore,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
|
|||||||
@ -36,16 +36,18 @@ import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
|||||||
import { WorkloadsOverviewDetailRegistry } from "../../../extensions/registries";
|
import { WorkloadsOverviewDetailRegistry } from "../../../extensions/registries";
|
||||||
import type { WorkloadsOverviewRouteParams } from "../../../common/routes";
|
import type { WorkloadsOverviewRouteParams } from "../../../common/routes";
|
||||||
import { makeObservable, observable, reaction } from "mobx";
|
import { makeObservable, observable, reaction } from "mobx";
|
||||||
import { clusterContext } from "../context";
|
|
||||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { TooltipPosition } from "../tooltip";
|
import { TooltipPosition } from "../tooltip";
|
||||||
|
import type { ClusterContext } from "../../../common/k8s-api/cluster-context";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<WorkloadsOverviewRouteParams> {
|
interface Props extends RouteComponentProps<WorkloadsOverviewRouteParams> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class WorkloadsOverview extends React.Component<Props> {
|
export class WorkloadsOverview extends React.Component<Props> {
|
||||||
|
static clusterContext: ClusterContext;
|
||||||
|
|
||||||
@observable loadErrors: string[] = [];
|
@observable loadErrors: string[] = [];
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
@ -56,12 +58,18 @@ export class WorkloadsOverview extends React.Component<Props> {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
kubeWatchApi.subscribeStores([
|
kubeWatchApi.subscribeStores([
|
||||||
podsStore, deploymentStore, daemonSetStore, statefulSetStore, replicaSetStore,
|
cronJobStore,
|
||||||
jobStore, cronJobStore, eventStore,
|
daemonSetStore,
|
||||||
|
deploymentStore,
|
||||||
|
eventStore,
|
||||||
|
jobStore,
|
||||||
|
podsStore,
|
||||||
|
replicaSetStore,
|
||||||
|
statefulSetStore,
|
||||||
], {
|
], {
|
||||||
onLoadFailure: error => this.loadErrors.push(String(error)),
|
onLoadFailure: error => this.loadErrors.push(String(error)),
|
||||||
}),
|
}),
|
||||||
reaction(() => clusterContext.contextNamespaces.slice(), () => {
|
reaction(() => WorkloadsOverview.clusterContext.contextNamespaces.slice(), () => {
|
||||||
// clear load errors
|
// clear load errors
|
||||||
this.loadErrors.length = 0;
|
this.loadErrors.length = 0;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
|||||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||||
import { boundMethod } from "../../utils";
|
import { boundMethod } from "../../utils";
|
||||||
import logger from "../../../common/logger";
|
import logger from "../../../common/logger";
|
||||||
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<ReplicaSet> {
|
interface Props extends KubeObjectDetailsProps<ReplicaSet> {
|
||||||
}
|
}
|
||||||
@ -53,13 +54,15 @@ export class ReplicaSetDetails extends React.Component<Props> {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@disposeOnUnmount
|
componentDidMount() {
|
||||||
clean = reaction(() => this.props.object, () => {
|
disposeOnUnmount(this, [
|
||||||
this.metrics = null;
|
reaction(() => this.props.object, () => {
|
||||||
});
|
this.metrics = null;
|
||||||
|
}),
|
||||||
async componentDidMount() {
|
kubeWatchApi.subscribeStores([
|
||||||
podsStore.reloadAll();
|
podsStore,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
|
|||||||
@ -41,6 +41,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
|||||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||||
import { boundMethod } from "../../utils";
|
import { boundMethod } from "../../utils";
|
||||||
import logger from "../../../common/logger";
|
import logger from "../../../common/logger";
|
||||||
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<StatefulSet> {
|
interface Props extends KubeObjectDetailsProps<StatefulSet> {
|
||||||
}
|
}
|
||||||
@ -54,13 +55,15 @@ export class StatefulSetDetails extends React.Component<Props> {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@disposeOnUnmount
|
|
||||||
clean = reaction(() => this.props.object, () => {
|
|
||||||
this.metrics = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
podsStore.reloadAll();
|
disposeOnUnmount(this, [
|
||||||
|
reaction(() => this.props.object, () => {
|
||||||
|
this.metrics = null;
|
||||||
|
}),
|
||||||
|
kubeWatchApi.subscribeStores([
|
||||||
|
podsStore,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
|
|||||||
@ -25,7 +25,6 @@ import { computed, observable, makeObservable } from "mobx";
|
|||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
||||||
import { ClusterStore } from "../../../common/cluster-store";
|
|
||||||
import { ipcRendererOn, requestMain } from "../../../common/ipc";
|
import { ipcRendererOn, requestMain } from "../../../common/ipc";
|
||||||
import type { Cluster } from "../../../main/cluster";
|
import type { Cluster } from "../../../main/cluster";
|
||||||
import { cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
@ -34,11 +33,12 @@ import { Icon } from "../icon";
|
|||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { entitySettingsURL } from "../../../common/routes";
|
import { entitySettingsURL } from "../../../common/routes";
|
||||||
import type { ClusterId, KubeAuthUpdate } from "../../../common/cluster-types";
|
import type { KubeAuthUpdate } from "../../../common/cluster-types";
|
||||||
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: IClassName;
|
className?: IClassName;
|
||||||
clusterId: ClusterId;
|
cluster: Cluster;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -52,7 +52,11 @@ export class ClusterStatus extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get cluster(): Cluster {
|
get cluster(): Cluster {
|
||||||
return ClusterStore.getInstance().getById(this.props.clusterId);
|
return this.props.cluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get entity() {
|
||||||
|
return catalogEntityRegistry.getById(this.cluster.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get hasErrors(): boolean {
|
@computed get hasErrors(): boolean {
|
||||||
@ -72,7 +76,7 @@ export class ClusterStatus extends React.Component<Props> {
|
|||||||
this.isReconnecting = true;
|
this.isReconnecting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await requestMain(clusterActivateHandler, this.props.clusterId, true);
|
await requestMain(clusterActivateHandler, this.cluster.id, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.authOutput.push({
|
this.authOutput.push({
|
||||||
message: error.toString(),
|
message: error.toString(),
|
||||||
@ -86,7 +90,7 @@ export class ClusterStatus extends React.Component<Props> {
|
|||||||
manageProxySettings = () => {
|
manageProxySettings = () => {
|
||||||
navigate(entitySettingsURL({
|
navigate(entitySettingsURL({
|
||||||
params: {
|
params: {
|
||||||
entityId: this.props.clusterId,
|
entityId: this.cluster.id,
|
||||||
},
|
},
|
||||||
fragment: "proxy",
|
fragment: "proxy",
|
||||||
}));
|
}));
|
||||||
@ -149,7 +153,7 @@ export class ClusterStatus extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<div className={cssNames(styles.status, "flex column box center align-center justify-center", this.props.className)}>
|
<div className={cssNames(styles.status, "flex column box center align-center justify-center", this.props.className)}>
|
||||||
<div className="flex items-center column gaps">
|
<div className="flex items-center column gaps">
|
||||||
<h2>{this.cluster.preferences.clusterName}</h2>
|
<h2>{this.entity.getName()}</h2>
|
||||||
{this.renderStatusIcon()}
|
{this.renderStatusIcon()}
|
||||||
{this.renderAuthenticationOutput()}
|
{this.renderAuthenticationOutput()}
|
||||||
{this.renderReconnectionHelp()}
|
{this.renderReconnectionHelp()}
|
||||||
|
|||||||
@ -89,10 +89,10 @@ export class ClusterView extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderStatus(): React.ReactNode {
|
renderStatus(): React.ReactNode {
|
||||||
const { clusterId, cluster, isReady } = this;
|
const { cluster, isReady } = this;
|
||||||
|
|
||||||
if (cluster && !isReady) {
|
if (cluster && !isReady) {
|
||||||
return <ClusterStatus key={clusterId} clusterId={clusterId} className="box center"/>;
|
return <ClusterStatus cluster={cluster} className="box center"/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -19,24 +19,19 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ClusterStore } from "../../common/cluster-store";
|
|
||||||
import type { Cluster } from "../../main/cluster";
|
import type { Cluster } from "../../main/cluster";
|
||||||
import { getHostedClusterId } from "../utils";
|
|
||||||
import { namespaceStore } from "./+namespaces/namespace.store";
|
import { namespaceStore } from "./+namespaces/namespace.store";
|
||||||
import type { ClusterContext } from "../../common/k8s-api/cluster-context";
|
import type { ClusterContext } from "../../common/k8s-api/cluster-context";
|
||||||
|
import { computed, makeObservable } from "mobx";
|
||||||
|
|
||||||
export const clusterContext: ClusterContext = {
|
export class FrameContext implements ClusterContext {
|
||||||
get cluster(): Cluster | null {
|
constructor(public cluster: Cluster) {
|
||||||
return ClusterStore.getInstance().getById(getHostedClusterId());
|
makeObservable(this);
|
||||||
},
|
}
|
||||||
|
|
||||||
get allNamespaces(): string[] {
|
|
||||||
if (!this.cluster) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@computed get allNamespaces(): string[] {
|
||||||
// user given list of namespaces
|
// user given list of namespaces
|
||||||
if (this.cluster?.accessibleNamespaces.length) {
|
if (this.cluster.accessibleNamespaces.length) {
|
||||||
return this.cluster.accessibleNamespaces;
|
return this.cluster.accessibleNamespaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,9 +42,17 @@ export const clusterContext: ClusterContext = {
|
|||||||
// fallback to cluster resolved namespaces because we could not load list
|
// fallback to cluster resolved namespaces because we could not load list
|
||||||
return this.cluster.allowedNamespaces || [];
|
return this.cluster.allowedNamespaces || [];
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
get contextNamespaces(): string[] {
|
@computed get contextNamespaces(): string[] {
|
||||||
return namespaceStore.contextNamespaces ?? [];
|
return namespaceStore.contextNamespaces;
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
@computed get hasSelectedAll(): boolean {
|
||||||
|
const namespaces = new Set(this.contextNamespaces);
|
||||||
|
|
||||||
|
return this.allNamespaces?.length > 1
|
||||||
|
&& this.cluster.accessibleNamespaces.length === 0
|
||||||
|
&& this.allNamespaces.every(ns => namespaces.has(ns));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -52,15 +52,12 @@ type Props = {};
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class DeleteClusterDialog extends React.Component {
|
export class DeleteClusterDialog extends React.Component {
|
||||||
showContextSwitch = false;
|
@observable showContextSwitch = false;
|
||||||
newCurrentContext = "";
|
@observable newCurrentContext = "";
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this, {
|
makeObservable(this);
|
||||||
showContextSwitch: observable,
|
|
||||||
newCurrentContext: observable,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static open({ config, cluster }: Partial<DialogState>) {
|
static open({ config, cluster }: Partial<DialogState>) {
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as uuid from "uuid";
|
import * as uuid from "uuid";
|
||||||
import { action, comparer, computed, IReactionOptions, makeObservable, observable, reaction, runInAction } from "mobx";
|
import { action, comparer, computed, makeObservable, observable, reaction, runInAction } from "mobx";
|
||||||
import { autoBind, createStorage } from "../../utils";
|
import { autoBind, createStorage } from "../../utils";
|
||||||
import throttle from "lodash/throttle";
|
import throttle from "lodash/throttle";
|
||||||
|
|
||||||
@ -94,7 +94,11 @@ export interface DockTabChangeEvent {
|
|||||||
prevTab?: DockTab;
|
prevTab?: DockTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DockTabChangeEventOptions extends IReactionOptions {
|
export interface DockTabChangeEventOptions {
|
||||||
|
/**
|
||||||
|
* apply a callback right after initialization
|
||||||
|
*/
|
||||||
|
fireImmediately?: boolean;
|
||||||
/**
|
/**
|
||||||
* filter: by dockStore.selectedTab.kind == tabKind
|
* filter: by dockStore.selectedTab.kind == tabKind
|
||||||
*/
|
*/
|
||||||
@ -195,11 +199,13 @@ export class DockStore implements DockStorageState {
|
|||||||
if (this.height > this.maxHeight) this.height = this.maxHeight;
|
if (this.height > this.maxHeight) this.height = this.maxHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
onResize(callback: () => void, options?: IReactionOptions) {
|
onResize(callback: () => void, opts: { fireImmediately?: boolean } = {}) {
|
||||||
return reaction(() => [this.height, this.fullSize], callback, options);
|
return reaction(() => [this.height, this.fullSize], callback, {
|
||||||
|
fireImmediately: opts.fireImmediately,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onTabClose(callback: (evt: DockTabCloseEvent) => void, options: IReactionOptions = {}) {
|
onTabClose(callback: (evt: DockTabCloseEvent) => void, opts: { fireImmediately?: boolean } = {}) {
|
||||||
return reaction(() => dockStore.tabs.map(tab => tab.id), (tabs: TabId[], prevTabs?: TabId[]) => {
|
return reaction(() => dockStore.tabs.map(tab => tab.id), (tabs: TabId[], prevTabs?: TabId[]) => {
|
||||||
if (!Array.isArray(prevTabs)) {
|
if (!Array.isArray(prevTabs)) {
|
||||||
return; // tabs not yet modified
|
return; // tabs not yet modified
|
||||||
@ -214,7 +220,7 @@ export class DockStore implements DockStorageState {
|
|||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
equals: comparer.structural,
|
equals: comparer.structural,
|
||||||
...options,
|
fireImmediately: opts.fireImmediately,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,14 @@
|
|||||||
width: var(--hotbar-width);
|
width: var(--hotbar-width);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.draggingOver::after {
|
||||||
|
content: " ";
|
||||||
|
position: fixed;
|
||||||
|
left: var(--hotbar-width);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.HotbarItems {
|
.HotbarItems {
|
||||||
--cellWidth: 40px;
|
--cellWidth: 40px;
|
||||||
--cellHeight: 40px;
|
--cellHeight: 40px;
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import { HotbarSelector } from "./hotbar-selector";
|
|||||||
import { HotbarCell } from "./hotbar-cell";
|
import { HotbarCell } from "./hotbar-cell";
|
||||||
import { HotbarIcon } from "./hotbar-icon";
|
import { HotbarIcon } from "./hotbar-icon";
|
||||||
import { defaultHotbarCells, HotbarItem } from "../../../common/hotbar-types";
|
import { defaultHotbarCells, HotbarItem } from "../../../common/hotbar-types";
|
||||||
|
import { action, makeObservable, observable } from "mobx";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: IClassName;
|
className?: IClassName;
|
||||||
@ -40,6 +41,13 @@ interface Props {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class HotbarMenu extends React.Component<Props> {
|
export class HotbarMenu extends React.Component<Props> {
|
||||||
|
@observable draggingOver = false;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
get hotbar() {
|
get hotbar() {
|
||||||
return HotbarStore.getInstance().getActive();
|
return HotbarStore.getInstance().getActive();
|
||||||
}
|
}
|
||||||
@ -54,9 +62,17 @@ export class HotbarMenu extends React.Component<Props> {
|
|||||||
return catalogEntityRegistry.getById(item?.entity.uid) ?? null;
|
return catalogEntityRegistry.getById(item?.entity.uid) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onDragStart() {
|
||||||
|
this.draggingOver = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
onDragEnd(result: DropResult) {
|
onDragEnd(result: DropResult) {
|
||||||
const { source, destination } = result;
|
const { source, destination } = result;
|
||||||
|
|
||||||
|
this.draggingOver = false;
|
||||||
|
|
||||||
if (!destination) { // Dropped outside of the list
|
if (!destination) { // Dropped outside of the list
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -165,9 +181,9 @@ export class HotbarMenu extends React.Component<Props> {
|
|||||||
const hotbar = hotbarStore.getActive();
|
const hotbar = hotbarStore.getActive();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("HotbarMenu flex column", className)}>
|
<div className={cssNames("HotbarMenu flex column", { draggingOver: this.draggingOver }, className)}>
|
||||||
<div className="HotbarItems flex column gaps">
|
<div className="HotbarItems flex column gaps">
|
||||||
<DragDropContext onDragEnd={this.onDragEnd.bind(this)}>
|
<DragDropContext onDragStart={() => this.onDragStart()} onDragEnd={(result) => this.onDragEnd(result)}>
|
||||||
{this.renderGrid()}
|
{this.renderGrid()}
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -30,12 +30,12 @@ import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-li
|
|||||||
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||||
import { KubeObjectMenu } from "../kube-object-menu";
|
import { KubeObjectMenu } from "../kube-object-menu";
|
||||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
import { clusterContext } from "../context";
|
|
||||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||||
import { ResourceKindMap, ResourceNames } from "../../utils/rbac";
|
import { ResourceKindMap, ResourceNames } from "../../utils/rbac";
|
||||||
import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params";
|
import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { TooltipPosition } from "../tooltip";
|
import { TooltipPosition } from "../tooltip";
|
||||||
|
import type { ClusterContext } from "../../../common/k8s-api/cluster-context";
|
||||||
|
|
||||||
export interface KubeObjectListLayoutProps<K extends KubeObject> extends ItemListLayoutProps<K> {
|
export interface KubeObjectListLayoutProps<K extends KubeObject> extends ItemListLayoutProps<K> {
|
||||||
store: KubeObjectStore<K>;
|
store: KubeObjectStore<K>;
|
||||||
@ -51,6 +51,7 @@ const defaultProps: Partial<KubeObjectListLayoutProps<KubeObject>> = {
|
|||||||
@observer
|
@observer
|
||||||
export class KubeObjectListLayout<K extends KubeObject> extends React.Component<KubeObjectListLayoutProps<K>> {
|
export class KubeObjectListLayout<K extends KubeObject> extends React.Component<KubeObjectListLayoutProps<K>> {
|
||||||
static defaultProps = defaultProps as object;
|
static defaultProps = defaultProps as object;
|
||||||
|
static clusterContext: ClusterContext;
|
||||||
|
|
||||||
constructor(props: KubeObjectListLayoutProps<K>) {
|
constructor(props: KubeObjectListLayoutProps<K>) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -67,7 +68,7 @@ export class KubeObjectListLayout<K extends KubeObject> extends React.Component<
|
|||||||
const { store, dependentStores = [], subscribeStores } = this.props;
|
const { store, dependentStores = [], subscribeStores } = this.props;
|
||||||
const stores = Array.from(new Set([store, ...dependentStores]));
|
const stores = Array.from(new Set([store, ...dependentStores]));
|
||||||
const reactions: Disposer[] = [
|
const reactions: Disposer[] = [
|
||||||
reaction(() => clusterContext.contextNamespaces.slice(), () => {
|
reaction(() => KubeObjectListLayout.clusterContext.contextNamespaces.slice(), () => {
|
||||||
// clear load errors
|
// clear load errors
|
||||||
this.loadErrors.length = 0;
|
this.loadErrors.length = 0;
|
||||||
}),
|
}),
|
||||||
@ -76,8 +77,6 @@ export class KubeObjectListLayout<K extends KubeObject> extends React.Component<
|
|||||||
if (subscribeStores) {
|
if (subscribeStores) {
|
||||||
reactions.push(
|
reactions.push(
|
||||||
kubeWatchApi.subscribeStores(stores, {
|
kubeWatchApi.subscribeStores(stores, {
|
||||||
preload: true,
|
|
||||||
namespaces: clusterContext.contextNamespaces,
|
|
||||||
onLoadFailure: error => this.loadErrors.push(String(error)),
|
onLoadFailure: error => this.loadErrors.push(String(error)),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -54,7 +54,9 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
kubeWatchApi.subscribeStores([crdStore]),
|
kubeWatchApi.subscribeStores([
|
||||||
|
crdStore,
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -50,6 +50,7 @@
|
|||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
padding-left: $padding;
|
padding-left: $padding;
|
||||||
padding-right: $padding * 2;
|
padding-right: $padding * 2;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@ -63,9 +64,5 @@
|
|||||||
box-shadow: 0 0 20px var(--boxShadow);
|
box-shadow: 0 0 20px var(--boxShadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
|
||||||
margin-top: -2px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
yarn.lock
13
yarn.lock
@ -9535,10 +9535,10 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.5"
|
minimist "^1.2.5"
|
||||||
|
|
||||||
mobx-observable-history@^2.0.1:
|
mobx-observable-history@^2.0.3:
|
||||||
version "2.0.1"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/mobx-observable-history/-/mobx-observable-history-2.0.1.tgz#bb39f19346e094c3608bbfba4b2b0aca985a562f"
|
resolved "https://registry.yarnpkg.com/mobx-observable-history/-/mobx-observable-history-2.0.3.tgz#07dd551e9d2a5666ca1d759ad108173fab47125e"
|
||||||
integrity sha512-ijNuz2iBl5SYRdvVIqK3yeRJEGxWS7Oqq14ideNUDAvNZUBk/iCOmzWMgR6vfwp3bYieX/6tANS8N6RjVTKwGg==
|
integrity sha512-cWMG3GcT1l2Y880mfffNh9m6WldQyOtlLUvcdVUjIj++sNOQbRxKBaBUe/TPDiJ80EN6g8FGiVuFlzzyRJPykQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/history" "^4.7.8"
|
"@types/history" "^4.7.8"
|
||||||
history "^4.10.1"
|
history "^4.10.1"
|
||||||
@ -9561,6 +9561,11 @@ mobx@^6.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.0.tgz#a8fb693c3047bdfcb1eaff9aa48e36a7eb084f96"
|
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.0.tgz#a8fb693c3047bdfcb1eaff9aa48e36a7eb084f96"
|
||||||
integrity sha512-Aa1+VXsg4WxqJMTQfWoYuJi5UD10VZhiobSmcs5kcmI3BIT0aVtn7DysvCeDADCzl7dnbX+0BTHUj/v7gLlZpQ==
|
integrity sha512-Aa1+VXsg4WxqJMTQfWoYuJi5UD10VZhiobSmcs5kcmI3BIT0aVtn7DysvCeDADCzl7dnbX+0BTHUj/v7gLlZpQ==
|
||||||
|
|
||||||
|
mobx@^6.3.7:
|
||||||
|
version "6.3.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.7.tgz#9ed85561e86da45141134c8fa20cf5f9c7246c3d"
|
||||||
|
integrity sha512-X7yU7eOEyxIBk4gjIi2UIilwdw48gXh0kcZ5ex3Rc+COJsJmJ4SNpf42uYea3aUqb1hedTv5xzJrq5Q55p0P5g==
|
||||||
|
|
||||||
mock-fs@^4.14.0:
|
mock-fs@^4.14.0:
|
||||||
version "4.14.0"
|
version "4.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18"
|
resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user