1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into set-kubectl-binary-path

This commit is contained in:
Lauri Nevala 2020-09-04 12:41:47 +03:00 committed by GitHub
commit 51199154e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 148 additions and 46 deletions

View File

@ -6,7 +6,7 @@ endif
.PHONY: init dev build test clean .PHONY: init dev build test clean
init: download-bins install-deps compile-dev init: install-deps download-bins compile-dev
echo "Init done" echo "Init done"
download-bins: download-bins:
@ -46,7 +46,7 @@ integration-win:
test-app: test-app:
yarn test yarn test
build: install-deps build: install-deps download-bins
yarn install yarn install
ifeq "$(DETECTED_OS)" "Windows" ifeq "$(DETECTED_OS)" "Windows"
yarn dist:win yarn dist:win

View File

@ -10,9 +10,9 @@ Lens is the only IDE youll ever need to take control of your Kubernetes clust
## What makes Lens special? ## What makes Lens special?
* Amazing usability and end user experience * Amazing usability and end-user experience
* Multi cluster management; Support for hundreds of clusters * Multi cluster management: support for hundreds of clusters
* Standalone application; No need to install anything in-cluster * Standalone application: no need to install anything in-cluster
* Real-time cluster state visualization * Real-time cluster state visualization
* Resource utilization charts and trends with history powered by built-in Prometheus * Resource utilization charts and trends with history powered by built-in Prometheus
* Terminal access to nodes and containers * Terminal access to nodes and containers
@ -38,7 +38,7 @@ brew cask install lens
## Development (advanced) ## Development (advanced)
Allows faster separately re-run some of involved processes: Allows for faster separate re-runs of some of the more involved processes:
1. `yarn dev:main` compiles electron's main process part and start watching files 1. `yarn dev:main` compiles electron's main process part and start watching files
1. `yarn dev:renderer` compiles electron's renderer part and start watching files 1. `yarn dev:renderer` compiles electron's renderer part and start watching files

View File

@ -11,7 +11,7 @@ const fileOptions: winston.transports.FileTransportOptions = {
handleExceptions: false, handleExceptions: false,
level: isDebugging ? "debug" : "info", level: isDebugging ? "debug" : "info",
filename: "lens.log", filename: "lens.log",
dirname: (app || remote.app).getPath("logs"), dirname: (app ?? remote?.app)?.getPath("logs"),
maxsize: 16 * 1024, maxsize: 16 * 1024,
maxFiles: 16, maxFiles: 16,
tailable: true, tailable: true,

View File

@ -42,7 +42,7 @@ export function buildMenu(windowManager: WindowManager) {
`${appName}: ${app.getVersion()}`, `${appName}: ${app.getVersion()}`,
`Electron: ${process.versions.electron}`, `Electron: ${process.versions.electron}`,
`Chrome: ${process.versions.chrome}`, `Chrome: ${process.versions.chrome}`,
`Copyright 2020 Copyright 2020 Mirantis, Inc.`, `Copyright 2020 Mirantis, Inc.`,
] ]
dialog.showMessageBoxSync(browserWindow, { dialog.showMessageBoxSync(browserWindow, {
title: `${isWindows ? " ".repeat(2) : ""}${appName}`, title: `${isWindows ? " ".repeat(2) : ""}${appName}`,

View File

@ -1,13 +1,28 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { VersionedKubeApi } from "../kube-api-versioned";
import { crdResourcesURL } from "../../components/+custom-resources/crd.route"; import { crdResourcesURL } from "../../components/+custom-resources/crd.route";
type AdditionalPrinterColumnsCommon = {
name: string;
type: "integer" | "number" | "string" | "boolean" | "date";
priority: number;
description: string;
}
type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & {
jsonPath: string;
}
type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
JSONPath: string;
}
export class CustomResourceDefinition extends KubeObject { export class CustomResourceDefinition extends KubeObject {
static kind = "CustomResourceDefinition"; static kind = "CustomResourceDefinition";
spec: { spec: {
group: string; group: string;
version: string; version?: string; // deprecated in v1 api
names: { names: {
plural: string; plural: string;
singular: string; singular: string;
@ -20,18 +35,14 @@ export class CustomResourceDefinition extends KubeObject {
name: string; name: string;
served: boolean; served: boolean;
storage: boolean; storage: boolean;
schema?: unknown; // required in v1 but not present in v1beta
additionalPrinterColumns?: AdditionalPrinterColumnsV1[]
}[]; }[];
conversion: { conversion: {
strategy?: string; strategy?: string;
webhook?: any; webhook?: any;
}; };
additionalPrinterColumns?: { additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; // removed in v1
name: string;
type: "integer" | "number" | "string" | "boolean" | "date";
priority: number;
description: string;
JSONPath: string;
}[];
} }
status: { status: {
conditions: { conditions: {
@ -61,8 +72,8 @@ export class CustomResourceDefinition extends KubeObject {
} }
getResourceApiBase() { getResourceApiBase() {
const { version, group } = this.spec; const { group } = this.spec;
return `/apis/${group}/${version}/${this.getPluralName()}` return `/apis/${group}/${this.getVersion()}/${this.getPluralName()}`
} }
getPluralName() { getPluralName() {
@ -87,7 +98,8 @@ export class CustomResourceDefinition extends KubeObject {
} }
getVersion() { getVersion() {
return this.spec.version; // v1 has removed the spec.version property, if it is present it must match the first version
return this.spec.versions[0]?.name ?? this.spec.version;
} }
isNamespaced() { isNamespaced() {
@ -107,14 +119,16 @@ export class CustomResourceDefinition extends KubeObject {
} }
getPrinterColumns(ignorePriority = true) { getPrinterColumns(ignorePriority = true) {
const columns = this.spec.additionalPrinterColumns || []; const columns = this.spec.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
?? this.spec.additionalPrinterColumns?.map(({JSONPath, ...rest}) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
?? [];
return columns return columns
.filter(column => column.name != "Age") .filter(column => column.name != "Age")
.filter(column => ignorePriority ? true : !column.priority); .filter(column => ignorePriority ? true : !column.priority);
} }
getValidation() { getValidation() {
return JSON.stringify(this.spec.validation, null, 2); return JSON.stringify(this.spec.validation ?? this.spec.versions?.[0]?.schema, null, 2);
} }
getConditions() { getConditions() {
@ -130,16 +144,10 @@ export class CustomResourceDefinition extends KubeObject {
} }
} }
export const crdBetaApi = new KubeApi<CustomResourceDefinition>({ export const crdApi = new VersionedKubeApi<CustomResourceDefinition>({
kind: CustomResourceDefinition.kind,
apiBase: "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions",
isNamespaced: false,
objectConstructor: CustomResourceDefinition,
});
export const crdApi = new KubeApi<CustomResourceDefinition>({
kind: CustomResourceDefinition.kind, kind: CustomResourceDefinition.kind,
apiBase: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions", apiBase: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions",
isNamespaced: false, isNamespaced: false,
objectConstructor: CustomResourceDefinition, objectConstructor: CustomResourceDefinition
}); });

View File

@ -0,0 +1,56 @@
import { stringify } from "querystring";
import { KubeObject } from "./kube-object";
import { createKubeApiURL } from "./kube-api-parse";
import { KubeApi, IKubeApiQueryParams, IKubeApiOptions } from "./kube-api";
import { apiManager } from "./api-manager";
export class VersionedKubeApi<T extends KubeObject = any> extends KubeApi<T> {
private preferredVersion?: string;
constructor(opts: IKubeApiOptions<T>) {
super(opts);
this.getPreferredVersion().then(() => {
if (this.apiBase != opts.apiBase)
apiManager.registerApi(this.apiBase, this);
});
}
// override this property to make read-write
apiBase: string
async getPreferredVersion() {
if (this.preferredVersion) return;
const apiGroupVersion = await this.request.get<{ preferredVersion?: { version: string; }; }>(`${this.apiPrefix}/${this.apiGroup}`);
if (!apiGroupVersion?.preferredVersion) return;
this.preferredVersion = apiGroupVersion.preferredVersion.version;
// update apiBase
this.apiBase = this.getUrl();
}
async list({ namespace = "" } = {}, query?: IKubeApiQueryParams): Promise<T[]> {
await this.getPreferredVersion();
return await super.list({namespace}, query);
}
async get({ name = "", namespace = "default" } = {}, query?: IKubeApiQueryParams): Promise<T> {
await this.getPreferredVersion();
return super.get({ name, namespace }, query);
}
getUrl({ name = "", namespace = "" } = {}, query?: Partial<IKubeApiQueryParams>) {
const { apiPrefix, apiGroup, apiVersion, apiResource, preferredVersion, isNamespaced } = this;
const resourcePath = createKubeApiURL({
apiPrefix: apiPrefix,
apiVersion: `${apiGroup}/${preferredVersion ?? apiVersion}`,
resource: apiResource,
namespace: isNamespaced ? namespace : undefined,
name: name,
});
return resourcePath + (query ? `?` + stringify(query) : "");
}
}

View File

@ -25,7 +25,6 @@
.version { .version {
.Select { .Select {
width: 80px;
min-width: 80px; min-width: 80px;
white-space: nowrap; white-space: nowrap;
} }

View File

@ -1,7 +1,6 @@
import "./cluster-settings.scss"; import "./cluster-settings.scss";
import React from "react"; import React from "react";
import { Link } from "react-router-dom";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Features } from "./features"; import { Features } from "./features";
import { Removal } from "./removal"; import { Removal } from "./removal";
@ -15,6 +14,25 @@ import { navigate } from "../../navigation";
@observer @observer
export class ClusterSettings extends React.Component { export class ClusterSettings extends React.Component {
async componentDidMount() {
window.addEventListener('keydown', this.onEscapeKey);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.onEscapeKey);
}
onEscapeKey = (evt: KeyboardEvent) => {
if (evt.code === "Escape") {
evt.stopPropagation();
this.close();
}
}
close() {
navigate("/");
}
render() { render() {
const cluster = getMatchedCluster(); const cluster = getMatchedCluster();
if (!cluster) return null; if (!cluster) return null;
@ -26,7 +44,7 @@ export class ClusterSettings extends React.Component {
showTooltip={false} showTooltip={false}
/> />
<h2>{cluster.preferences.clusterName}</h2> <h2>{cluster.preferences.clusterName}</h2>
<Icon material="close" onClick={() => navigate("/")} big/> <Icon material="close" onClick={this.close} big/>
</> </>
); );
return ( return (

View File

@ -22,7 +22,7 @@ export class Status extends React.Component<Props> {
const rows = [ const rows = [
["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"})`], ["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"})`],
["Distribution", cluster.distribution], ["Distribution", cluster.distribution],
["Kerbel Version", cluster.version], ["Kernel Version", cluster.version],
["API Address", cluster.apiUrl], ["API Address", cluster.apiUrl],
["Nodes Count", cluster.nodes || "0"] ["Nodes Count", cluster.nodes || "0"]
]; ];

View File

@ -102,13 +102,13 @@ export class CRDDetails extends React.Component<Props> {
</TableHead> </TableHead>
{ {
printerColumns.map((column, index) => { printerColumns.map((column, index) => {
const { name, type, JSONPath } = column; const { name, type, jsonPath } = column;
return ( return (
<TableRow key={index}> <TableRow key={index}>
<TableCell className="name">{name}</TableCell> <TableCell className="name">{name}</TableCell>
<TableCell className="type">{type}</TableCell> <TableCell className="type">{type}</TableCell>
<TableCell className="json-path"> <TableCell className="json-path">
<Badge label={JSONPath}/> <Badge label={jsonPath}/>
</TableCell> </TableCell>
</TableRow> </TableRow>
) )

View File

@ -57,7 +57,7 @@ export class CrdResourceDetails extends React.Component<Props> {
<KubeObjectMeta object={object}/> <KubeObjectMeta object={object}/>
{extraColumns.map(column => { {extraColumns.map(column => {
const { name } = column; const { name } = column;
const value = jsonPath.query(object, column.JSONPath.slice(1)); const value = jsonPath.query(object, (column.jsonPath).slice(1));
return ( return (
<DrawerItem key={name} name={name}> <DrawerItem key={name} name={name}>
<CrdColumnValue value={value} /> <CrdColumnValue value={value} />

View File

@ -57,7 +57,7 @@ export class CrdResources extends React.Component<Props> {
[sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp, [sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp,
} }
extraColumns.forEach(column => { extraColumns.forEach(column => {
sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.query(item, column.JSONPath.slice(1)) sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.query(item, column.jsonPath.slice(1))
}) })
// todo: merge extra columns and other params to predefined view // todo: merge extra columns and other params to predefined view
const { List } = apiManager.getViews(crd.getResourceApiBase()); const { List } = apiManager.getViews(crd.getResourceApiBase());
@ -88,9 +88,9 @@ export class CrdResources extends React.Component<Props> {
renderTableContents={(crdInstance: KubeObject) => [ renderTableContents={(crdInstance: KubeObject) => [
crdInstance.getName(), crdInstance.getName(),
isNamespaced && crdInstance.getNs(), isNamespaced && crdInstance.getNs(),
...extraColumns.map(column => ...extraColumns.map(column => {
jsonPath.query(crdInstance, column.JSONPath.slice(1)) return jsonPath.query(crdInstance, (column.jsonPath).slice(1))
), }),
crdInstance.getAge(), crdInstance.getAge(),
]} ]}
renderItemMenu={(item: KubeObject) => { renderItemMenu={(item: KubeObject) => {

View File

@ -23,6 +23,7 @@ export class Preferences extends React.Component {
@observable helmLoading = false; @observable helmLoading = false;
@observable helmRepos: HelmRepo[] = []; @observable helmRepos: HelmRepo[] = [];
@observable helmAddedRepos = observable.map<string, HelmRepo>(); @observable helmAddedRepos = observable.map<string, HelmRepo>();
@observable httpProxy = userStore.preferences.httpsProxy || "";
@computed get themeOptions(): SelectOption<string>[] { @computed get themeOptions(): SelectOption<string>[] {
return themeStore.themes.map(theme => ({ return themeStore.themes.map(theme => ({
@ -39,9 +40,21 @@ export class Preferences extends React.Component {
} }
async componentDidMount() { async componentDidMount() {
window.addEventListener('keydown', this.onEscapeKey);
await this.loadHelmRepos(); await this.loadHelmRepos();
} }
componentWillUnmount() {
window.removeEventListener('keydown', this.onEscapeKey);
}
onEscapeKey = (evt: KeyboardEvent) => {
if (evt.code === "Escape") {
evt.stopPropagation();
history.goBack();
}
}
@action @action
async loadHelmRepos() { async loadHelmRepos() {
this.helmLoading = true; this.helmLoading = true;
@ -121,11 +134,13 @@ export class Preferences extends React.Component {
<Input <Input
theme="round-black" theme="round-black"
placeholder={_i18n._(t`Type HTTP proxy url (example: http://proxy.acme.org:8080)`)} placeholder={_i18n._(t`Type HTTP proxy url (example: http://proxy.acme.org:8080)`)}
value={preferences.httpsProxy || ""} value={this.httpProxy}
onChange={v => preferences.httpsProxy = v} onChange={v => this.httpProxy = v}
onBlur={() => preferences.httpsProxy = this.httpProxy}
/> />
<small className="hint"> <small className="hint">
<Trans>Proxy is used only for non-cluster communication.</Trans> <Trans>Proxy is used only for non-cluster communication.</Trans>
</small>Proxy is used only for non-cluster communication.</Trans>
</small> </small>
<KubectlBinaries preferences={preferences} /> <KubectlBinaries preferences={preferences} />

View File

@ -5,7 +5,7 @@
} }
&.chart-version { &.chart-version {
min-width: 80px; min-width: 130px;
} }
} }
} }

View File

@ -70,11 +70,15 @@ html {
&__menu { &__menu {
background: var(--select-menu-bgc); background: var(--select-menu-bgc);
box-shadow: inset 0 0 0 1px var(--select-menu-border-color); box-shadow: inset 0 0 0 1px var(--select-menu-border-color);
width: max-content;
min-width: 100%;
&-list { &-list {
@include custom-scrollbar; @include custom-scrollbar;
padding-right: 1px; padding-right: 1px;
padding-left: 1px; padding-left: 1px;
width: max-content;
min-width: 100%;
} }
&-notice { &-notice {
@ -83,6 +87,8 @@ html {
} }
&__option { &__option {
white-space: nowrap;
&:active { &:active {
background: $primary; background: $primary;
} }