Merge remote-tracking branch 'origin/master' into extension_install_1277

# Conflicts:
#	src/common/utils/index.ts
This commit is contained in:
Roman committed 2020-11-23 23:32:10 +02:00
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");
});
});
+6 -3
View File
@@ -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
+3
View File
@@ -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";
+8
View File
@@ -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];
}
+12 -4
View File
@@ -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) {
+5 -1
View File
@@ -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;
}
+14 -1
View File
@@ -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);
+7 -6
View File
@@ -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
+5 -3
View File
@@ -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 }),
+1 -1
View File
@@ -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 => ({
+6
View File
@@ -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 -4
View File
@@ -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) {
-1
View File
@@ -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");
});
});
+70 -17
View File
@@ -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);
+91 -3
View File
@@ -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>
+1 -1
View File
@@ -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;
}
}
+14 -7
View File
@@ -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}
/>
+1 -1
View File
@@ -67,7 +67,7 @@
"terminalForeground": "#2d2d2d",
"terminalCursor": "#2d2d2d",
"terminalCursorAccent": "#ffffff",
"terminalSelection": "#bfbfbf",
"terminalSelection": "#bfbfbf66",
"terminalBlack": "#2d2d2d",
"terminalRed": "#cd3734 ",
"terminalGreen": "#18cf12",
-1
View File
@@ -1,6 +1,5 @@
// Common usage utils & helpers
export const noop: any = Function();
export const isElectron = !!navigator.userAgent.match(/Electron/);
export * from "../../common/utils";