mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge remote-tracking branch 'origin/master' into extension_install_1277
# Conflicts: # src/common/utils/index.ts
This commit is contained in:
commit
de93812ee3
58 files changed
+891
-8895
No files matched your search
@@ -65,6 +65,7 @@ describe("empty config", () => {
|
||||
it("sets active cluster", () => {
|
||||
clusterStore.setActive("foo");
|
||||
expect(clusterStore.active.id).toBe("foo");
|
||||
expect(workspaceStore.currentWorkspace.lastActiveClusterId).toBe("foo");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { WorkspaceId } from "./workspace-store";
|
||||
import { workspaceStore } from "./workspace-store";
|
||||
import path from "path";
|
||||
import { app, ipcRenderer, remote, webFrame } from "electron";
|
||||
import { unlink } from "fs-extra";
|
||||
@@ -11,9 +11,10 @@ import { appEventBus } from "./event-bus";
|
||||
import { dumpConfigYaml } from "./kube-helpers";
|
||||
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
import { subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
||||
import _ from "lodash";
|
||||
import move from "array-move";
|
||||
import { subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
||||
import type { WorkspaceId } from "./workspace-store";
|
||||
|
||||
export interface ClusterIconUpload {
|
||||
clusterId: string;
|
||||
@@ -142,7 +143,9 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
|
||||
@action
|
||||
setActive(id: ClusterId) {
|
||||
this.activeCluster = this.clusters.has(id) ? id : null;
|
||||
const clusterId = this.clusters.has(id) ? id : null;
|
||||
this.activeCluster = clusterId;
|
||||
workspaceStore.setLastActiveClusterId(clusterId);
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Common utils (main OR renderer)
|
||||
|
||||
export const noop: any = () => { /* empty */ };
|
||||
|
||||
export * from "./app-version";
|
||||
export * from "./autobind";
|
||||
export * from "./base64";
|
||||
@@ -12,4 +14,5 @@ export * from "./splitArray";
|
||||
export * from "./saveToAppFiles";
|
||||
export * from "./singleton";
|
||||
export * from "./openExternal";
|
||||
export * from "./rectify-array";
|
||||
export * from "./downloadFile";
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* rectify condences the single item or array of T type, to an array.
|
||||
* @param items either one item or an array of items
|
||||
* @returns a list of items
|
||||
*/
|
||||
export function recitfy<T>(items: T | T[]): T[] {
|
||||
return Array.isArray(items) ? items : [items];
|
||||
}
|
||||
@@ -5,12 +5,13 @@ import { clusterStore } from "./cluster-store";
|
||||
import { appEventBus } from "./event-bus";
|
||||
import { broadcastMessage } from "../common/ipc";
|
||||
import logger from "../main/logger";
|
||||
import type { ClusterId } from "./cluster-store";
|
||||
|
||||
export type WorkspaceId = string;
|
||||
|
||||
export interface WorkspaceStoreModel {
|
||||
workspaces: WorkspaceModel[];
|
||||
currentWorkspace?: WorkspaceId;
|
||||
workspaces: WorkspaceModel[]
|
||||
}
|
||||
|
||||
export interface WorkspaceModel {
|
||||
@@ -18,6 +19,7 @@ export interface WorkspaceModel {
|
||||
name: string;
|
||||
description?: string;
|
||||
ownerRef?: string;
|
||||
lastActiveClusterId?: ClusterId;
|
||||
}
|
||||
|
||||
export interface WorkspaceState {
|
||||
@@ -30,6 +32,7 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
|
||||
@observable description?: string;
|
||||
@observable ownerRef?: string;
|
||||
@observable enabled: boolean;
|
||||
@observable lastActiveClusterId?: ClusterId;
|
||||
|
||||
constructor(data: WorkspaceModel) {
|
||||
Object.assign(this, data);
|
||||
@@ -66,7 +69,8 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
ownerRef: this.ownerRef
|
||||
ownerRef: this.ownerRef,
|
||||
lastActiveClusterId: this.lastActiveClusterId
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -138,13 +142,12 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||
}
|
||||
|
||||
@action
|
||||
setActive(id = WorkspaceStore.defaultId, reset = true) {
|
||||
setActive(id = WorkspaceStore.defaultId) {
|
||||
if (id === this.currentWorkspaceId) return;
|
||||
if (!this.getById(id)) {
|
||||
throw new Error(`workspace ${id} doesn't exist`);
|
||||
}
|
||||
this.currentWorkspaceId = id;
|
||||
clusterStore.activeCluster = null; // fixme: handle previously selected cluster from current workspace
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -184,6 +187,11 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||
clusterStore.removeByWorkspaceId(id);
|
||||
}
|
||||
|
||||
@action
|
||||
setLastActiveClusterId(clusterId?: ClusterId, workspaceId = this.currentWorkspaceId) {
|
||||
this.getById(workspaceId).lastActiveClusterId = clusterId;
|
||||
}
|
||||
|
||||
@action
|
||||
protected fromStore({ currentWorkspace, workspaces = [] }: WorkspaceStoreModel) {
|
||||
if (currentWorkspace) {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { getAppVersion } from "../../common/utils";
|
||||
import { extensionsStore } from "../extensions-store";
|
||||
|
||||
export const version = getAppVersion();
|
||||
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
|
||||
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
|
||||
export function getEnabledExtensions(): string[] {
|
||||
return extensionsStore.enabledExtensions;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LensExtensionId } from "./lens-extension";
|
||||
import type { ExtensionLoader } from "./extension-loader";
|
||||
import { BaseStore } from "../common/base-store";
|
||||
import { action, observable, reaction, toJS } from "mobx";
|
||||
import { action, computed, observable, reaction, toJS } from "mobx";
|
||||
|
||||
export interface LensExtensionsStoreModel {
|
||||
extensions: Record<LensExtensionId, LensExtensionState>;
|
||||
@@ -9,6 +9,7 @@ export interface LensExtensionsStoreModel {
|
||||
|
||||
export interface LensExtensionState {
|
||||
enabled?: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
||||
@@ -18,6 +19,17 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
||||
});
|
||||
}
|
||||
|
||||
@computed
|
||||
get enabledExtensions() {
|
||||
const extensions: string[] = [];
|
||||
this.state.forEach((state, id) => {
|
||||
if (state.enabled) {
|
||||
extensions.push(state.name);
|
||||
}
|
||||
});
|
||||
return extensions;
|
||||
}
|
||||
|
||||
protected state = observable.map<LensExtensionId, LensExtensionState>();
|
||||
|
||||
protected getState(extensionLoader: ExtensionLoader) {
|
||||
@@ -25,6 +37,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
||||
return Array.from(extensionLoader.userExtensions).reduce((state, [extId, ext]) => {
|
||||
state[extId] = {
|
||||
enabled: ext.isEnabled,
|
||||
name: ext.manifest.name,
|
||||
};
|
||||
return state;
|
||||
}, state);
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
// Base class for extensions-api registries
|
||||
import { action, observable } from "mobx";
|
||||
import { LensExtension } from "../lens-extension";
|
||||
import { recitfy } from "../../common/utils";
|
||||
|
||||
export class BaseRegistry<T = object, I extends T = T> {
|
||||
export class BaseRegistry<T> {
|
||||
private items = observable<T>([], { deep: false });
|
||||
|
||||
getItems(): I[] {
|
||||
return this.items.toJS() as I[];
|
||||
getItems(): T[] {
|
||||
return this.items.toJS();
|
||||
}
|
||||
|
||||
add(items: T | T[], ext?: LensExtension): () => void; // allow method overloading with required "ext"
|
||||
@action
|
||||
add(items: T | T[]) {
|
||||
const normalizedItems = (Array.isArray(items) ? items : [items]);
|
||||
this.items.push(...normalizedItems);
|
||||
return () => this.remove(...normalizedItems);
|
||||
const itemArray = recitfy(items);
|
||||
this.items.push(...itemArray);
|
||||
return () => this.remove(...itemArray);
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -7,6 +7,7 @@ import { compile } from "path-to-regexp";
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
import { LensExtension, sanitizeExtensionName } from "../lens-extension";
|
||||
import logger from "../../main/logger";
|
||||
import { recitfy } from "../../common/utils";
|
||||
|
||||
export interface PageRegistration {
|
||||
/**
|
||||
@@ -55,12 +56,13 @@ export function getExtensionPageUrl<P extends object>({ extensionId, pageId = ""
|
||||
return extPageRoutePath;
|
||||
}
|
||||
|
||||
export class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage> {
|
||||
export class PageRegistry extends BaseRegistry<RegisteredPage> {
|
||||
@action
|
||||
add(items: PageRegistration[], ext: LensExtension) {
|
||||
add(items: PageRegistration | PageRegistration[], ext: LensExtension) {
|
||||
const itemArray = recitfy(items);
|
||||
let registeredPages: RegisteredPage[] = [];
|
||||
try {
|
||||
registeredPages = items.map(page => ({
|
||||
registeredPages = itemArray.map(page => ({
|
||||
...page,
|
||||
extensionId: ext.name,
|
||||
routePath: getExtensionPageUrl({ extensionId: ext.name, pageId: page.id ?? page.routePath }),
|
||||
|
||||
@@ -83,7 +83,7 @@ export class HelmRepoManager extends Singleton {
|
||||
repositories: []
|
||||
}));
|
||||
if (!repositories.length) {
|
||||
await this.addRepo({ name: "stable", url: "https://kubernetes-charts.storage.googleapis.com/" });
|
||||
await this.addRepo({ name: "bitnami", url: "https://charts.bitnami.com/bitnami" });
|
||||
return await this.repositories();
|
||||
}
|
||||
return repositories.map(repo => ({
|
||||
|
||||
@@ -218,6 +218,12 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
shell.openExternal('https://docs.k8slens.dev/');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Support",
|
||||
click: async () => {
|
||||
shell.openExternal('https://docs.k8slens.dev/latest/support/');
|
||||
},
|
||||
},
|
||||
...ignoreOnMac([
|
||||
{
|
||||
label: "About Lens",
|
||||
|
||||
@@ -12,14 +12,22 @@ class HelmApiRoute extends LensApi {
|
||||
|
||||
public async getChart(request: LensApiRequest) {
|
||||
const { params, query, response } = request;
|
||||
const chart = await helmService.getChart(params.repo, params.chart, query.get("version"));
|
||||
this.respondJson(response, chart);
|
||||
try {
|
||||
const chart = await helmService.getChart(params.repo, params.chart, query.get("version"));
|
||||
this.respondJson(response, chart);
|
||||
} catch (error) {
|
||||
this.respondText(response, error, 422);
|
||||
}
|
||||
}
|
||||
|
||||
public async getChartValues(request: LensApiRequest) {
|
||||
const { params, query, response } = request;
|
||||
const values = await helmService.getChartValues(params.repo, params.chart, query.get("version"));
|
||||
this.respondJson(response, values);
|
||||
try {
|
||||
const values = await helmService.getChartValues(params.repo, params.chart, query.get("version"));
|
||||
this.respondJson(response, values);
|
||||
} catch (error) {
|
||||
this.respondText(response, error, 422);
|
||||
}
|
||||
}
|
||||
|
||||
public async installChart(request: LensApiRequest) {
|
||||
|
||||
@@ -95,7 +95,6 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
|
||||
toolTip: clusterId,
|
||||
async click() {
|
||||
workspaceStore.setActive(workspace);
|
||||
clusterStore.setActive(clusterId);
|
||||
windowManager.navigate(clusterViewURL({ params: { clusterId } }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
describe("KubeApi", () => {
|
||||
it("uses url from apiBase if apiBase contains the resource", async () => {
|
||||
(fetch as any).mockResponse(async (request: any) => {
|
||||
if (request.url === "/api-kube/apis/networking.k8s.io/v1") {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses"
|
||||
}] as any []
|
||||
})
|
||||
};
|
||||
} else if (request.url === "/api-kube/apis/extensions/v1beta1") {
|
||||
// Even if the old API contains ingresses, KubeApi should prefer the apiBase url
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses"
|
||||
}] as any []
|
||||
})
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any []
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
||||
const kubeApi = new KubeApi({
|
||||
apiBase,
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
|
||||
await kubeApi.get();
|
||||
expect(kubeApi.apiPrefix).toEqual("/apis");
|
||||
expect(kubeApi.apiGroup).toEqual("networking.k8s.io");
|
||||
});
|
||||
|
||||
it("uses url from fallbackApiBases if apiBase lacks the resource", async () => {
|
||||
(fetch as any).mockResponse(async (request: any) => {
|
||||
if (request.url === "/api-kube/apis/networking.k8s.io/v1") {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any []
|
||||
})
|
||||
};
|
||||
} else if (request.url === "/api-kube/apis/extensions/v1beta1") {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses"
|
||||
}] as any []
|
||||
})
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any []
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const apiBase = "apis/networking.k8s.io/v1/ingresses";
|
||||
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
||||
const kubeApi = new KubeApi({
|
||||
apiBase,
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
|
||||
await kubeApi.get();
|
||||
expect(kubeApi.apiPrefix).toEqual("/apis");
|
||||
expect(kubeApi.apiGroup).toEqual("extensions");
|
||||
});
|
||||
});
|
||||
@@ -29,11 +29,42 @@ export interface ILoadBalancerIngress {
|
||||
hostname?: string;
|
||||
ip?: string;
|
||||
}
|
||||
|
||||
// extensions/v1beta1
|
||||
interface IExtensionsBackend {
|
||||
serviceName: string;
|
||||
servicePort: number;
|
||||
}
|
||||
|
||||
// networking.k8s.io/v1
|
||||
interface INetworkingBackend {
|
||||
service: IIngressService;
|
||||
}
|
||||
|
||||
export type IIngressBackend = IExtensionsBackend | INetworkingBackend;
|
||||
|
||||
export interface IIngressService {
|
||||
name: string;
|
||||
port: {
|
||||
name?: string;
|
||||
number?: number;
|
||||
}
|
||||
}
|
||||
|
||||
export const getBackendServiceNamePort = (backend: IIngressBackend) => {
|
||||
// .service is available with networking.k8s.io/v1, otherwise using extensions/v1beta1 interface
|
||||
const serviceName = "service" in backend ? backend.service.name : backend.serviceName;
|
||||
// Port is specified either with a number or name
|
||||
const servicePort = "service" in backend ? backend.service.port.number ?? backend.service.port.name : backend.servicePort;
|
||||
|
||||
return { serviceName, servicePort };
|
||||
};
|
||||
|
||||
@autobind()
|
||||
export class Ingress extends KubeObject {
|
||||
static kind = "Ingress";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/extensions/v1beta1/ingresses";
|
||||
static apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||
|
||||
spec: {
|
||||
tls: {
|
||||
@@ -44,17 +75,20 @@ export class Ingress extends KubeObject {
|
||||
http: {
|
||||
paths: {
|
||||
path?: string;
|
||||
backend: {
|
||||
serviceName: string;
|
||||
servicePort: number;
|
||||
};
|
||||
backend: IIngressBackend;
|
||||
}[];
|
||||
};
|
||||
}[];
|
||||
backend?: {
|
||||
serviceName: string;
|
||||
servicePort: number;
|
||||
};
|
||||
// extensions/v1beta1
|
||||
backend?: IExtensionsBackend;
|
||||
// networking.k8s.io/v1
|
||||
defaultBackend?: INetworkingBackend & {
|
||||
resource: {
|
||||
apiGroup: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
}
|
||||
}
|
||||
};
|
||||
status: {
|
||||
loadBalancer: {
|
||||
@@ -75,7 +109,9 @@ export class Ingress extends KubeObject {
|
||||
const host = rule.host ? rule.host : "*";
|
||||
if (rule.http && rule.http.paths) {
|
||||
rule.http.paths.forEach(path => {
|
||||
routes.push(protocol + "://" + host + (path.path || "/") + " ⇢ " + path.backend.serviceName + ":" + path.backend.servicePort);
|
||||
const { serviceName, servicePort } = getBackendServiceNamePort(path.backend);
|
||||
|
||||
routes.push(protocol + "://" + host + (path.path || "/") + " ⇢ " + serviceName + ":" + servicePort);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -83,6 +119,17 @@ export class Ingress extends KubeObject {
|
||||
return routes;
|
||||
}
|
||||
|
||||
getServiceNamePort() {
|
||||
const { spec } = this;
|
||||
const serviceName = spec?.defaultBackend?.service.name ?? spec?.backend?.serviceName;
|
||||
const servicePort = spec?.defaultBackend?.service.port.number ?? spec?.defaultBackend?.service.port.name ?? spec?.backend?.servicePort;
|
||||
|
||||
return {
|
||||
serviceName,
|
||||
servicePort
|
||||
};
|
||||
}
|
||||
|
||||
getHosts() {
|
||||
const { spec: { rules } } = this;
|
||||
if (!rules) return [];
|
||||
@@ -91,22 +138,24 @@ export class Ingress extends KubeObject {
|
||||
|
||||
getPorts() {
|
||||
const ports: number[] = [];
|
||||
const { spec: { tls, rules, backend } } = this;
|
||||
const { spec: { tls, rules, backend, defaultBackend } } = this;
|
||||
const httpPort = 80;
|
||||
const tlsPort = 443;
|
||||
|
||||
// Note: not using the port name (string)
|
||||
const servicePort = defaultBackend?.service.port.number ?? backend?.servicePort;
|
||||
|
||||
if (rules && rules.length > 0) {
|
||||
if (rules.some(rule => rule.hasOwnProperty("http"))) {
|
||||
ports.push(httpPort);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (backend && backend.servicePort) {
|
||||
ports.push(backend.servicePort);
|
||||
}
|
||||
} else if (servicePort !== undefined) {
|
||||
ports.push(Number(servicePort));
|
||||
}
|
||||
if (tls && tls.length > 0) {
|
||||
ports.push(tlsPort);
|
||||
}
|
||||
|
||||
return ports.join(", ");
|
||||
}
|
||||
|
||||
@@ -121,4 +170,8 @@ export class Ingress extends KubeObject {
|
||||
|
||||
export const ingressApi = new IngressApi({
|
||||
objectConstructor: Ingress,
|
||||
});
|
||||
// Add fallback for Kubernetes <1.19
|
||||
checkPreferredVersion: true,
|
||||
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
|
||||
logStuff: true
|
||||
} as any);
|
||||
@@ -8,10 +8,22 @@ import { apiKube } from "./index";
|
||||
import { kubeWatchApi } from "./kube-watch-api";
|
||||
import { apiManager } from "./api-manager";
|
||||
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
|
||||
import { apiKubePrefix, isDevelopment } from "../../common/vars";
|
||||
import { apiKubePrefix, isDevelopment, isTestEnv } from "../../common/vars";
|
||||
|
||||
export interface IKubeApiOptions<T extends KubeObject> {
|
||||
apiBase?: string; // base api-path for listing all resources, e.g. "/api/v1/pods"
|
||||
/**
|
||||
* base api-path for listing all resources, e.g. "/api/v1/pods"
|
||||
*/
|
||||
apiBase?: string;
|
||||
|
||||
/**
|
||||
* If the API uses a different API endpoint (e.g. apiBase) depending on the cluster version,
|
||||
* fallback API bases can be listed individually.
|
||||
* The first (existing) API base is used in the requests, if apiBase is not found.
|
||||
* This option only has effect if checkPreferredVersion is true.
|
||||
*/
|
||||
fallbackApiBases?: string[];
|
||||
|
||||
objectConstructor?: IKubeObjectConstructor<T>;
|
||||
request?: KubeJsonApi;
|
||||
isNamespaced?: boolean;
|
||||
@@ -35,6 +47,17 @@ export interface IKubePreferredVersion {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IKubeResourceList {
|
||||
resources: {
|
||||
kind: string;
|
||||
name: string;
|
||||
namespaced: boolean;
|
||||
singularName: string;
|
||||
storageVersionHash: string;
|
||||
verbs: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface IKubeApiCluster {
|
||||
id: string;
|
||||
}
|
||||
@@ -85,7 +108,7 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
if (!options.apiBase) {
|
||||
options.apiBase = objectConstructor.apiBase;
|
||||
}
|
||||
const { apiBase, apiPrefix, apiGroup, apiVersion, apiVersionWithGroup, resource } = KubeApi.parseApi(options.apiBase);
|
||||
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = KubeApi.parseApi(options.apiBase);
|
||||
|
||||
this.kind = kind;
|
||||
this.isNamespaced = isNamespaced;
|
||||
@@ -108,8 +131,73 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
.join("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest API prefix/group that contains the required resource.
|
||||
* First tries options.apiBase, then urls in order from options.fallbackApiBases.
|
||||
*/
|
||||
private async getLatestApiPrefixGroup() {
|
||||
// Note that this.options.apiBase is the "full" url, whereas this.apiBase is parsed
|
||||
const apiBases = [this.options.apiBase, ...this.options.fallbackApiBases];
|
||||
|
||||
for (const apiUrl of apiBases) {
|
||||
// Split e.g. "/apis/extensions/v1beta1/ingresses" to parts
|
||||
const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = KubeApi.parseApi(apiUrl);
|
||||
|
||||
// Request available resources
|
||||
try {
|
||||
const response = await this.request.get<IKubeResourceList>(`${apiPrefix}/${apiVersionWithGroup}`);
|
||||
|
||||
// If the resource is found in the group, use this apiUrl
|
||||
if (response.resources?.find(kubeResource => kubeResource.name === resource)) {
|
||||
return { apiPrefix, apiGroup };
|
||||
}
|
||||
} catch (error) {
|
||||
// Exception is ignored as we can try the next url
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid throwing in tests
|
||||
if (isTestEnv) {
|
||||
return {
|
||||
apiPrefix: this.apiPrefix,
|
||||
apiGroup: this.apiGroup
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Can't find working API for the Kubernetes resource ${this.apiResource}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the apiPrefix and apiGroup to be used for fetching the preferred version.
|
||||
*/
|
||||
private async getPreferredVersionPrefixGroup() {
|
||||
if (this.options.fallbackApiBases) {
|
||||
return this.getLatestApiPrefixGroup();
|
||||
} else {
|
||||
return {
|
||||
apiPrefix: this.apiPrefix,
|
||||
apiGroup: this.apiGroup
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected async checkPreferredVersion() {
|
||||
if (this.options.fallbackApiBases && !this.options.checkPreferredVersion) {
|
||||
throw new Error("checkPreferredVersion must be enabled if fallbackApiBases is set in KubeApi");
|
||||
}
|
||||
|
||||
if (this.options.checkPreferredVersion && this.apiVersionPreferred === undefined) {
|
||||
const { apiPrefix, apiGroup } = await this.getPreferredVersionPrefixGroup();
|
||||
|
||||
// The apiPrefix and apiGroup might change due to fallbackApiBases, so we must override them
|
||||
Object.defineProperty(this, "apiPrefix", {
|
||||
value: apiPrefix
|
||||
});
|
||||
Object.defineProperty(this, "apiGroup", {
|
||||
value: apiGroup
|
||||
});
|
||||
|
||||
const res = await this.request.get<IKubePreferredVersion>(`${this.apiPrefix}/${this.apiGroup}`);
|
||||
Object.defineProperty(this, "apiVersionPreferred", {
|
||||
value: res?.preferredVersion?.version ?? null,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { IngressCharts } from "./ingress-charts";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import { getBackendServiceNamePort } from "../../api/endpoints/ingress.api";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Ingress> {
|
||||
}
|
||||
@@ -48,7 +49,9 @@ export class IngressDetails extends React.Component<Props> {
|
||||
</TableHead>
|
||||
{
|
||||
rule.http.paths.map((path, index) => {
|
||||
const backend = `${path.backend.serviceName}:${path.backend.servicePort}`;
|
||||
const { serviceName, servicePort } = getBackendServiceNamePort(path.backend);
|
||||
const backend =`${serviceName}:${servicePort}`;
|
||||
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="path">{path.path || ""}</TableCell>
|
||||
@@ -100,6 +103,9 @@ export class IngressDetails extends React.Component<Props> {
|
||||
<Trans>Network</Trans>,
|
||||
<Trans>Duration</Trans>,
|
||||
];
|
||||
|
||||
const { serviceName, servicePort } = ingress.getServiceNamePort();
|
||||
|
||||
return (
|
||||
<div className="IngressDetails">
|
||||
<ResourceMetrics
|
||||
@@ -117,9 +123,9 @@ export class IngressDetails extends React.Component<Props> {
|
||||
{spec.tls.map((tls, index) => <p key={index}>{tls.secretName}</p>)}
|
||||
</DrawerItem>
|
||||
}
|
||||
{spec.backend && spec.backend.serviceName && spec.backend.servicePort &&
|
||||
{serviceName && servicePort &&
|
||||
<DrawerItem name={<Trans>Service</Trans>}>
|
||||
{spec.backend.serviceName}:{spec.backend.servicePort}
|
||||
{serviceName}:{servicePort}
|
||||
</DrawerItem>
|
||||
}
|
||||
<DrawerTitle title={<Trans>Rules</Trans>}/>
|
||||
@@ -134,14 +140,14 @@ export class IngressDetails extends React.Component<Props> {
|
||||
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "Ingress",
|
||||
apiVersions: ["extensions/v1beta1"],
|
||||
apiVersions: ["networking.k8s.io/v1", "extensions/v1beta1"],
|
||||
components: {
|
||||
Details: (props) => <IngressDetails {...props} />
|
||||
}
|
||||
});
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "Ingress",
|
||||
apiVersions: ["extensions/v1beta1"],
|
||||
apiVersions: ["networking.k8s.io/v1", "extensions/v1beta1"],
|
||||
priority: 5,
|
||||
components: {
|
||||
Details: (props) => <KubeEventDetails {...props} />
|
||||
|
||||
@@ -7,8 +7,11 @@ import { Trans } from "@lingui/macro";
|
||||
import { Menu, MenuItem, MenuProps } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { observable } from "mobx";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
import { WorkspaceId, workspaceStore } from "../../../common/workspace-store";
|
||||
import { cssNames } from "../../utils";
|
||||
import { navigate } from "../../navigation";
|
||||
import { clusterViewURL } from "../cluster-manager/cluster-view.route";
|
||||
import { landingURL } from "../+landing-page";
|
||||
|
||||
interface Props extends Partial<MenuProps> {
|
||||
}
|
||||
@@ -17,6 +20,16 @@ interface Props extends Partial<MenuProps> {
|
||||
export class WorkspaceMenu extends React.Component<Props> {
|
||||
@observable menuVisible = false;
|
||||
|
||||
activateWorkspace = (id: WorkspaceId) => {
|
||||
const clusterId = workspaceStore.getById(id).lastActiveClusterId;
|
||||
workspaceStore.setActive(id);
|
||||
if (clusterId) {
|
||||
navigate(clusterViewURL({ params: { clusterId } }));
|
||||
} else {
|
||||
navigate(landingURL());
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, ...menuProps } = this.props;
|
||||
const { enabledWorkspacesList, currentWorkspace } = workspaceStore;
|
||||
@@ -38,7 +51,7 @@ export class WorkspaceMenu extends React.Component<Props> {
|
||||
key={workspaceId}
|
||||
title={description}
|
||||
active={workspaceId === currentWorkspace.id}
|
||||
onClick={() => workspaceStore.setActive(workspaceId)}
|
||||
onClick={() => this.activateWorkspace(workspaceId)}
|
||||
>
|
||||
<Icon small material="layers"/>
|
||||
<span className="workspace">{name}</span>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Input } from "../input";
|
||||
import { cssNames, prevDefault } from "../../utils";
|
||||
import { Button } from "../button";
|
||||
import { isRequired, InputValidator } from "../input/input_validators";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
|
||||
@observer
|
||||
export class Workspaces extends React.Component {
|
||||
@@ -70,6 +71,12 @@ export class Workspaces extends React.Component {
|
||||
this.editingWorkspaces.set(id, toJS(workspace));
|
||||
};
|
||||
|
||||
activateWorkspace = (id: WorkspaceId) => {
|
||||
const clusterId = workspaceStore.getById(id).lastActiveClusterId;
|
||||
workspaceStore.setActive(id);
|
||||
clusterStore.setActive(clusterId);
|
||||
};
|
||||
|
||||
clearEditing = (id: WorkspaceId) => {
|
||||
this.editingWorkspaces.delete(id);
|
||||
};
|
||||
@@ -135,7 +142,7 @@ export class Workspaces extends React.Component {
|
||||
{!isEditing && (
|
||||
<Fragment>
|
||||
<span className="name flex gaps align-center">
|
||||
<a href="#" onClick={prevDefault(() => workspaceStore.setActive(workspaceId))}>{name}</a>
|
||||
<a href="#" onClick={prevDefault(() => this.activateWorkspace(workspaceId))}>{name}</a>
|
||||
{isActive && <span> <Trans>(current)</Trans></span>}
|
||||
</span>
|
||||
<span className="description">{description}</span>
|
||||
|
||||
@@ -100,7 +100,7 @@ export class App extends React.Component {
|
||||
const tabRoutes = this.getTabLayoutRoutes(menu);
|
||||
if (tabRoutes.length > 0) {
|
||||
const pageComponent = () => <TabLayout tabs={tabRoutes} />;
|
||||
return <Route key={"extension-tab-layout-route-" + index} component={pageComponent}/>;
|
||||
return <Route key={"extension-tab-layout-route-" + index} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)} />;
|
||||
} else {
|
||||
const page = clusterPageRegistry.getByPageMenuTarget(menu.target);
|
||||
if (page) {
|
||||
|
||||
@@ -77,6 +77,7 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
ok: () => {
|
||||
if (clusterStore.activeClusterId === cluster.id) {
|
||||
navigate(landingURL());
|
||||
clusterStore.setActive(null);
|
||||
}
|
||||
clusterStore.removeById(cluster.id);
|
||||
},
|
||||
|
||||
@@ -18,11 +18,4 @@
|
||||
.xterm-viewport {
|
||||
@include custom-scrollbar;
|
||||
}
|
||||
|
||||
// fix: safari won't handle paste event for textarea with zero size block
|
||||
.xterm-helper-textarea {
|
||||
width: 10px !important;
|
||||
height: 10px !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -98,23 +98,30 @@ export class Sidebar extends React.Component<Props> {
|
||||
}
|
||||
|
||||
renderRegisteredMenus() {
|
||||
return clusterPageMenuRegistry.getRootItems().map((menuItem) => {
|
||||
return clusterPageMenuRegistry.getRootItems().map((menuItem, index) => {
|
||||
const registeredPage = clusterPageRegistry.getByPageMenuTarget(menuItem.target);
|
||||
const tabRoutes = this.getTabLayoutRoutes(menuItem);
|
||||
let pageUrl: string;
|
||||
let routePath: string;
|
||||
let isActive = false;
|
||||
if (registeredPage) {
|
||||
const { extensionId, id: pageId } = registeredPage;
|
||||
pageUrl = getExtensionPageUrl({ extensionId, pageId, params: menuItem.target.params });
|
||||
isActive = pageUrl === navigation.location.pathname;
|
||||
}
|
||||
const tabRoutes = this.getTabLayoutRoutes(menuItem);
|
||||
if (!registeredPage && tabRoutes.length == 0) {
|
||||
routePath = registeredPage.routePath;
|
||||
isActive = isActiveRoute(registeredPage.routePath);
|
||||
} else if (tabRoutes.length > 0) {
|
||||
pageUrl = tabRoutes[0].url;
|
||||
routePath = tabRoutes[0].routePath;
|
||||
isActive = isActiveRoute(tabRoutes.map((tab) => tab.routePath));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<SidebarNavItem
|
||||
key={pageUrl} url={pageUrl}
|
||||
text={menuItem.title} icon={<menuItem.components.Icon/>}
|
||||
key={"registered-item-" + index}
|
||||
url={pageUrl}
|
||||
text={menuItem.title}
|
||||
icon={<menuItem.components.Icon/>}
|
||||
isActive={isActive}
|
||||
subMenus={tabRoutes}
|
||||
/>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"terminalForeground": "#2d2d2d",
|
||||
"terminalCursor": "#2d2d2d",
|
||||
"terminalCursorAccent": "#ffffff",
|
||||
"terminalSelection": "#bfbfbf",
|
||||
"terminalSelection": "#bfbfbf66",
|
||||
"terminalBlack": "#2d2d2d",
|
||||
"terminalRed": "#cd3734 ",
|
||||
"terminalGreen": "#18cf12",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Common usage utils & helpers
|
||||
|
||||
export const noop: any = Function();
|
||||
export const isElectron = !!navigator.userAgent.match(/Electron/);
|
||||
|
||||
export * from "../../common/utils";
|
||||
|
||||
Reference in new issue
Block a user