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

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

# Conflicts:
#	package.json
#	src/main/index.ts
#	src/renderer/api/__test__/parseAPI.test.ts
#	src/renderer/components/chart/bar-chart.tsx
#	src/renderer/components/chart/chart.tsx
#	src/renderer/components/chart/pie-chart.tsx
#	src/renderer/utils/__tests__/arrays.test.ts
#	src/renderer/utils/arrays.ts
#	yarn.lock
This commit is contained in:
Roman 2020-06-22 17:09:07 +03:00
commit f4922811cc
38 changed files with 281 additions and 265 deletions

View File

@ -13,7 +13,6 @@ trigger:
- "*"
jobs:
- job: Windows
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
pool:
vmImage: windows-2019
strategy:
@ -34,10 +33,14 @@ jobs:
inputs:
key: yarn | $(Agent.OS) | yarn.lock
path: $(YARN_CACHE_FOLDER)
cacheHitVar: CACHE_RESTORED
displayName: Cache Yarn packages
- script: make deps
displayName: Install dependencies
- script: make integration-win
displayName: Run integration tests
- script: make build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
displayName: Build
env:
WIN_CSC_LINK: $(WIN_CSC_LINK)
@ -53,6 +56,7 @@ jobs:
steps:
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
displayName: Set the tag name as an environment variable
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
- task: NodeTool@0
inputs:
versionSpec: $(node_version)
@ -138,6 +142,7 @@ jobs:
SNAP_LOGIN: $(SNAP_LOGIN)
- script: make build
displayName: Build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env:
GH_TOKEN: $(GH_TOKEN)
- bash: |

View File

@ -1,6 +1,8 @@
# Lens | The Kubernetes IDE
[![Build Status](https://dev.azure.com/lensapp/lensapp/_apis/build/status/lensapp.lens?branchName=master)](https://dev.azure.com/lensapp/lensapp/_build/latest?definitionId=1&branchName=master)
[![Releases](https://img.shields.io/github/downloads/lensapp/lens/total.svg)](https://github.com/lensapp/lens/releases)
[![Chat on Slack](https://img.shields.io/badge/chat-on%20slack-blue.svg?logo=slack&longCache=true&style=flat)](https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI)
Lens is the only IDE youll ever need to take control of your Kubernetes clusters. It is a standalone application for MacOS, Windows and Linux operating systems. It is open source and free.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -4,6 +4,12 @@
"description": "Lens - The Kubernetes IDE",
"version": "3.5.0-beta.1",
"main": "dist/main.js",
"copyright": "© 2020, Lakend Labs, Inc.",
"license": "MIT",
"author": {
"name": "Lakend Labs, Inc.",
"email": "info@lakendlabs.com"
},
"scripts": {
"dev": "concurrently 'yarn dev:main' 'yarn dev:renderer'",
"dev-run": "electron --inspect .",
@ -30,9 +36,6 @@
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
"download:helm": "yarn run ts-node build/download_helm.ts"
},
"author": "Lakend Labs, Inc. <info@lakendlabs.com>",
"copyright": "© 2020, Lakend Labs, Inc.",
"license": "MIT",
"config": {
"bundledKubectlVersion": "1.17.4",
"bundledHelmVersion": "3.1.2"

View File

@ -3,7 +3,7 @@ import { Application } from "spectron";
let appPath = ""
switch(process.platform) {
case "win32":
appPath = "./dist/win-unpacked/Lens.exe"
appPath = "./dist/win-unpacked/LensDev.exe"
break
case "linux":
appPath = "./dist/linux-unpacked/kontena-lens"

View File

@ -5,6 +5,8 @@ import { stat } from "fs"
jest.setTimeout(20000)
const BACKSPACE = "\uE003"
describe("app start", () => {
let app: Application
const clickWhatsNew = async (app: Application) => {
@ -13,6 +15,24 @@ describe("app start", () => {
await app.client.waitUntilTextExists("h1", "Welcome")
}
const addMinikubeCluster = async (app: Application) => {
await app.client.click("a#add-cluster")
await app.client.waitUntilTextExists("legend", "Choose config:")
await app.client.selectByVisibleText("select#kubecontext-select", "minikube (new)")
await app.client.click("button.btn-primary")
}
const waitForMinikubeDashboard = async (app: Application) => {
await app.client.waitUntilTextExists("pre.auth-output", "Authentication proxy started")
let windowCount = await app.client.getWindowCount()
// wait for webview to appear on window count
while (windowCount == 1) {
windowCount = await app.client.getWindowCount()
}
await app.client.windowByIndex(windowCount - 1)
await app.client.waitUntilTextExists("span.link-text", "Cluster")
}
beforeEach(async () => {
app = util.setup()
await app.start()
@ -32,22 +52,48 @@ describe("app start", () => {
return
}
await clickWhatsNew(app)
await app.client.click("a#add-cluster")
await app.client.waitUntilTextExists("legend", "Choose config:")
await app.client.selectByVisibleText("select#kubecontext-select", "minikube (new)")
await app.client.click("button.btn-primary")
await app.client.waitUntilTextExists("pre.auth-output", "Authentication proxy started")
let windowCount = await app.client.getWindowCount()
// wait for webview to appear on window count
while (windowCount == 1) {
windowCount = await app.client.getWindowCount()
}
await app.client.windowByIndex(windowCount - 1)
await app.client.waitUntilTextExists("span.link-text", "Cluster")
await addMinikubeCluster(app)
await waitForMinikubeDashboard(app)
await app.client.click('a[href="/nodes"]')
await app.client.waitUntilTextExists("div.TableCell", "minikube")
})
it('allows to create a pod', async () => {
const status = spawnSync("minikube status", {shell: true})
if (status.status !== 0) {
console.warn("minikube not running, skipping test")
return
}
await clickWhatsNew(app)
await addMinikubeCluster(app)
await waitForMinikubeDashboard(app)
await app.client.click(".sidebar-nav #workloads span.link-text")
await app.client.waitUntilTextExists('a[href="/pods"]', "Pods")
await app.client.click('a[href="/pods"]')
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver-minikube")
await app.client.click('.Icon.new-dock-tab')
await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource")
await app.client.click("li.MenuItem.create-resource-tab")
await app.client.waitForVisible(".CreateResource div.ace_content")
// Write pod manifest to editor
await app.client.keys("apiVersion: v1\n")
await app.client.keys("kind: Pod\n")
await app.client.keys("metadata:\n")
await app.client.keys(" name: nginx\n")
await app.client.keys(BACKSPACE + "spec:\n")
await app.client.keys(" containers:\n")
await app.client.keys("- name: nginx\n")
await app.client.keys(" image: nginx:alpine\n")
// Create deployent
await app.client.waitForEnabled("button.Button=Create & Close")
await app.client.click("button.Button=Create & Close")
// Wait until first bits of pod appears on dashboard
await app.client.waitForExist(".name=nginx")
// Open pod details
await app.client.click(".name=nginx")
await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx")
})
afterEach(async () => {
if (app && app.isRunning()) {
return util.tearDown(app)

View File

@ -29,6 +29,7 @@ if (app.commandLine.getSwitchValue("proxy-server") !== "") {
const promiseIpc = new PromiseIpc({ timeout: 2000 })
let windowManager: WindowManager = null;
let clusterManager: ClusterManager = null;
let proxyServer: proxy.LensProxy = null;
const vmURL = formatUrl({
pathname: path.join(__dirname, `${vueAppName}.html`),
@ -64,10 +65,9 @@ async function main() {
// create cluster manager
clusterManager = new ClusterManager(clusterStore.getAllClusterObjects(), port)
// run proxy
try {
proxy.listen(port, clusterManager)
proxyServer = proxy.listen(port, clusterManager)
} catch (error) {
logger.error(`Could not start proxy (127.0.0:${port}): ${error.message}`)
await dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${port}): ${error.message || "unknown error"}`)
@ -87,7 +87,7 @@ async function main() {
},
showPreferencesHook: async () => {
// IPC send needs webContents as we're sending it to renderer
promiseIpc.send('navigate', findMainWebContents(), { name: 'preferences-page' }).then((data: any) => {
promiseIpc.send('navigate', findMainWebContents(), {name: 'preferences-page'}).then((data: any) => {
logger.debug("navigate: preferences IPC sent");
})
},
@ -110,10 +110,10 @@ async function main() {
}
app.on("ready", main)
app.on('window-all-closed', function () {
app.on('window-all-closed', function() {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (!isMac) {
if (process.platform != 'darwin') {
app.quit();
} else {
windowManager = null
@ -130,5 +130,6 @@ app.on("activate", () => {
app.on("will-quit", async (event) => {
event.preventDefault(); // To allow mixpanel sending to be executed
if (clusterManager) clusterManager.stop()
if (proxyServer) proxyServer.close()
app.exit(0);
})

View File

@ -18,7 +18,7 @@ export class PrometheusLens implements PrometheusProvider {
port: service.spec.ports[0].port
}
} catch(error) {
logger.warn(`PrometheusLens: failed to list services: ${error.toString()}`)
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`)
}
}

View File

@ -18,6 +18,8 @@ export class LensProxy {
protected clusterManager: ClusterManager
protected retryCounters: Map<string, number> = new Map()
protected router: Router
protected proxyServer: http.Server
protected closed = false
constructor(port: number, clusterManager: ClusterManager) {
this.port = port
@ -28,6 +30,13 @@ export class LensProxy {
public run() {
const proxyServer = this.buildProxyServer();
proxyServer.listen(this.port, "127.0.0.1")
this.proxyServer = proxyServer
}
public close() {
logger.info("Closing proxy server")
this.proxyServer.close()
this.closed = true
}
protected buildProxyServer() {
@ -68,6 +77,9 @@ export class LensProxy {
}
})
proxy.on("error", (error, req, res, target) => {
if(this.closed) {
return
}
if (target) {
logger.debug("Failed proxy to target: " + JSON.stringify(target))
if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {

View File

@ -11,7 +11,7 @@ import { helmCli } from "./helm-cli"
import { isWindows } from "../common/vars";
export class ShellSession extends EventEmitter {
static shellEnv: any
static shellEnvs: Map<string, any> = new Map()
protected websocket: WebSocket
protected shellProcess: pty.IPty
@ -22,6 +22,7 @@ export class ShellSession extends EventEmitter {
protected helmBinDir: string;
protected preferences: ClusterPreferences;
protected running = false;
protected clusterId: string;
constructor(socket: WebSocket, pathToKubeconfig: string, cluster: Cluster) {
super()
@ -29,6 +30,7 @@ export class ShellSession extends EventEmitter {
this.kubeconfigPath = pathToKubeconfig
this.kubectl = new Kubectl(cluster.version)
this.preferences = cluster.preferences || {}
this.clusterId = cluster.id
}
public async open() {
@ -77,16 +79,14 @@ export class ShellSession extends EventEmitter {
}
protected async getCachedShellEnv() {
let env: any
if (!ShellSession.shellEnv) {
let env = ShellSession.shellEnvs.get(this.clusterId)
if (!env) {
env = await this.getShellEnv()
ShellSession.shellEnv = env
ShellSession.shellEnvs.set(this.clusterId, env)
} else {
env = ShellSession.shellEnv
// refresh env in the background
this.getShellEnv().then((shellEnv: any) => {
ShellSession.shellEnv = shellEnv
ShellSession.shellEnvs.set(this.clusterId, shellEnv)
})
}

View File

@ -122,7 +122,6 @@
id="checkbox-allow-telemetry"
switch
v-model="preferences.allowTelemetry"
:disabled="licenceData && licenceData.status === 'valid'"
@input="onSave"
>
Allow telemetry & usage tracking

View File

@ -47,6 +47,11 @@ export default new Vuex.Store({
this.commit("savePreferences", userStore.getPreferences());
},
savePreferences(state, prefs) {
if (prefs.allowTelemetry) {
tracker.event("telemetry", "enabled")
} else {
tracker.event("telemetry", "disabled")
}
state.preferences = prefs;
userStore.setPreferences(prefs);
this.dispatch("destroyWebviews")

View File

@ -156,9 +156,12 @@ export class HelmRelease implements ItemObject {
}
getChart(withVersion = false) {
return withVersion ?
this.chart :
this.chart.substr(0, this.chart.lastIndexOf("-"));
let chart = this.chart
if(!withVersion && this.getVersion() != "" ) {
const search = new RegExp(`-${this.getVersion()}`)
chart = chart.replace(search, "");
}
return chart
}
getRevision() {
@ -170,7 +173,7 @@ export class HelmRelease implements ItemObject {
}
getVersion() {
const versions = this.chart.match(/(\d+)[^-]*$/)
const versions = this.chart.match(/(v?\d+)[^-].*$/)
if (versions) {
return versions[0]
}

View File

@ -3,6 +3,7 @@ import { autobind } from "../../utils";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { IPodContainer } from "./pods.api";
import { KubeApi } from "../kube-api";
import { JsonApiParams } from "../json-api";
@autobind()
export class Job extends WorkloadKubeObject {
@ -88,6 +89,13 @@ export class Job extends WorkloadKubeObject {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", [])
return [...containers].map(container => container.image)
}
delete() {
const params: JsonApiParams = {
query: { propagationPolicy: "Background" }
}
return super.delete(params)
}
}
export const jobApi = new KubeApi({

View File

@ -56,7 +56,21 @@ export const metricsApi = {
};
export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
if (!metrics?.data?.result) {
return {
data: {
resultType: "",
result: [{
metric: {},
values: []
} as IMetricsResult],
},
status: "",
}
}
const { result } = metrics.data;
if (result.length) {
if (frames > 0) {
// fill the gaps
@ -81,13 +95,16 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
}
export function isMetricsEmpty(metrics: { [key: string]: IMetrics }) {
return Object.values(metrics).every(metric => !metric.data.result.length);
return Object.values(metrics).every(metric => !metric?.data?.result?.length);
}
export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: string) {
export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: string): { [key: string]: IMetrics } {
if (!metrics) return;
const itemMetrics = { ...metrics };
for (const metric in metrics) {
if (!metrics[metric]?.data?.result) {
continue
}
const results = metrics[metric].data.result;
const result = results.find(res => Object.values(res.metric)[0] == itemName);
itemMetrics[metric].data.result = result ? [result] : [];

View File

@ -7,6 +7,7 @@ import { IKubeObjectRef, KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } fro
import { apiKube } from "./index";
import { kubeWatchApi } from "./kube-watch-api";
import { apiManager } from "./api-manager";
import { split } from "../utils/arrays";
export interface IKubeApiOptions<T extends KubeObject> {
kind: string; // resource type within api-group, e.g. "Namespace"
@ -32,20 +33,45 @@ export interface IKubeApiLinkRef {
namespace?: string;
}
export class KubeApi<T extends KubeObject = any> {
static matcher = /(\/apis?.*?)\/(?:(.*?)\/)?(v.*?)(?:\/namespaces\/(.+?))?\/([^\/]+)(?:\/([^\/?]+))?.*$/
export interface IKubeApiLinkBase extends IKubeApiLinkRef {
apiBase: string;
apiGroup: string;
apiVersionWithGroup: string;
}
static parseApi(apiPath = "") {
export class KubeApi<T extends KubeObject = any> {
static parseApi(apiPath = ""): IKubeApiLinkBase {
apiPath = new URL(apiPath, location.origin).pathname;
const [, apiPrefix, apiGroup = "", apiVersion, namespace, resource, name] = apiPath.match(KubeApi.matcher) || [];
const [, prefix, ...parts] = apiPath.split("/");
const apiPrefix = `/${prefix}`;
const [left, right, found] = split(parts, "namespaces");
let apiGroup, apiVersion, namespace, resource, name;
if (found) {
if (left.length == 0) {
throw new Error(`invalid apiPath: ${apiPath}`)
}
apiVersion = left.pop();
apiGroup = left.join("/");
[namespace, resource, name] = right;
} else {
[apiGroup, apiVersion, resource] = left;
}
const apiVersionWithGroup = [apiGroup, apiVersion].filter(v => v).join("/");
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
if (!apiBase) {
throw new Error(`invalid apiPath: ${apiPath}`)
}
return {
apiBase,
apiPrefix, apiGroup,
apiVersion, apiVersionWithGroup,
namespace, resource, name,
}
};
}
static createLink(ref: IKubeApiLinkRef): string {
@ -55,7 +81,7 @@ export class KubeApi<T extends KubeObject = any> {
namespace = `namespaces/${namespace}`
}
return [apiPrefix, apiVersion, namespace, resource, name]
.filter(v => !!v)
.filter(v => v)
.join("/")
}
@ -130,8 +156,9 @@ export class KubeApi<T extends KubeObject = any> {
if (KubeObject.isJsonApiData(data)) {
return new KubeObjectConstructor(data);
}
// process items list response
else if (KubeObject.isJsonApiDataList(data)) {
if (KubeObject.isJsonApiDataList(data)) {
const { apiVersion, items, metadata } = data;
this.setResourceVersion(namespace, metadata.resourceVersion);
this.setResourceVersion("", metadata.resourceVersion);
@ -141,10 +168,12 @@ export class KubeApi<T extends KubeObject = any> {
...item,
}))
}
// custom apis might return array for list response, e.g. users, groups, etc.
else if (Array.isArray(data)) {
if (Array.isArray(data)) {
return data.map(data => new KubeObjectConstructor(data));
}
return data;
}
@ -162,16 +191,19 @@ export class KubeApi<T extends KubeObject = any> {
async create({ name = "", namespace = "default" } = {}, data?: Partial<T>): Promise<T> {
const apiUrl = this.getUrl({ namespace });
return this.request.post(apiUrl, {
data: merge({
kind: this.kind,
apiVersion: this.apiVersionWithGroup,
metadata: {
name,
namespace
}
}, data)
}).then(this.parseResponse);
return this.request
.post(apiUrl, {
data: merge({
kind: this.kind,
apiVersion: this.apiVersionWithGroup,
metadata: {
name,
namespace
}
}, data)
})
.then(this.parseResponse);
}
async update({ name = "", namespace = "default" } = {}, data?: Partial<T>): Promise<T> {

View File

@ -5,6 +5,7 @@ import { KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api";
import { autobind, formatDuration } from "../utils";
import { ItemObject } from "../item.store";
import { apiKube } from "./index";
import { JsonApiParams } from "./json-api";
import { resourceApplierApi } from "./endpoints/resource-applier.api";
export type IKubeObjectConstructor<T extends KubeObject = any> = (new (data: KubeJsonApiData | any) => T) & {
@ -153,7 +154,7 @@ export class KubeObject implements ItemObject {
});
}
delete() {
return apiKube.del(this.selfLink);
delete(params?: JsonApiParams) {
return apiKube.del(this.selfLink, params);
}
}

View File

@ -82,15 +82,15 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
this.liveMetrics = await this.loadMetrics({ start, end, step, range });
}
getMetricsValues(source: Partial<IClusterMetrics>) {
const metrics =
this.metricType === MetricType.CPU ? source.cpuUsage :
this.metricType === MetricType.MEMORY ? source.memoryUsage
: null;
if (!metrics) {
getMetricsValues(source: Partial<IClusterMetrics>): [number, string][] {
switch (this.metricType) {
case MetricType.CPU:
return normalizeMetrics(source.cpuUsage).data.result[0].values
case MetricType.MEMORY:
return normalizeMetrics(source.memoryUsage).data.result[0].values
default:
return [];
}
return normalizeMetrics(metrics).data.result[0].values;
}
resetMetrics() {

View File

@ -34,7 +34,7 @@ export class NamespaceSelect extends React.Component<Props> {
private unsubscribe = noop;
async componentDidMount() {
if (isAllowedResource("namespaces") && !namespaceStore.isLoaded) {
if (!namespaceStore.isLoaded) {
await namespaceStore.loadAll();
}
this.unsubscribe = namespaceStore.subscribe();

View File

@ -4,6 +4,7 @@ import { KubeObjectStore } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints";
import { IQueryParams, navigation, setQueryParams } from "../../navigation";
import { apiManager } from "../../api/api-manager";
import { isAllowedResource } from "../..//api/rbac";
@autobind()
export class NamespaceStore extends KubeObjectStore<Namespace> {
@ -43,6 +44,16 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}
protected loadItems(namespaces?: string[]) {
if (!isAllowedResource("namespaces")) {
if (namespaces) {
return Promise.all(namespaces.map(name => this.getDummyNamespace(name)))
}
else {
return new Promise<Namespace[]>(() => {
return []
})
}
}
if (namespaces) {
return Promise.all(namespaces.map(name => this.api.get({ name })))
}
@ -51,6 +62,19 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}
}
protected getDummyNamespace(name: string) {
return new Namespace({
kind: "Namespace",
apiVersion: "v1",
metadata: {
name: name,
uid: "",
resourceVersion: "",
selfLink: `/api/v1/namespaces/${name}`
}
})
}
setContext(namespaces: string[]) {
this.contextNs.replace(namespaces);
}

View File

@ -18,9 +18,9 @@ export const IngressCharts = observer(() => {
if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const values = Object.values(metrics).map(metric =>
normalizeMetrics(metric).data.result[0].values
);
const values = Object.values(metrics)
.map(normalizeMetrics)
.map(({ data }) => data.result[0].values);
const [
bytesSentSuccess,
bytesSentFailure,

View File

@ -96,7 +96,7 @@ export class DeploymentScaleDialog extends Component<Props> {
<Trans>Desired number of replicas</Trans>: {desiredReplicas}
</div>
<div className="slider-container">
<Slider value={desiredReplicas} max={scaleMax} onChange={onChange}/>
<Slider value={desiredReplicas} max={scaleMax} onChange={onChange as any /** see: https://github.com/mui-org/material-ui/issues/20191 */}/>
</div>
</div>
{warning &&

View File

@ -17,9 +17,9 @@ export const ContainerCharts = () => {
if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const values = Object.values(metrics).map(metric =>
normalizeMetrics(metric).data.result[0].values
);
const values = Object.values(metrics)
.map(normalizeMetrics)
.map(({ data }) => data.result[0].values);
const [
cpuUsage,
cpuRequests,

View File

@ -28,9 +28,9 @@ export const PodCharts = observer(() => {
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const options = tabId == 0 ? cpuOptions : memoryOptions;
const values = Object.values(metrics).map(metric =>
normalizeMetrics(metric).data.result[0].values
);
const values = Object.values(metrics)
.map(normalizeMetrics)
.map(({ data }) => data.result[0].values);
const [
cpuUsage,
cpuRequests,

View File

@ -120,8 +120,11 @@ const SecretKey = (props: SecretKeyProps) => {
setSecret(secret)
}
if (!secret) {
return (
if (secret?.data?.[key]) {
return <>{base64.decode(secret.data[key])}</>
}
return (
<>
secretKeyRef({name}.{key})&nbsp;
<Icon
@ -131,7 +134,5 @@ const SecretKey = (props: SecretKeyProps) => {
onClick={showKey}
/>
</>
)
}
return <>{base64.decode(secret.data[key])}</>
}
)
}

View File

@ -139,7 +139,7 @@ export function BarChart(props: Props) {
}
};
const options = merge(barOptions, customOptions);
if (!chartData.datasets.length) {
if (chartData.datasets.length == 0) {
return <NoMetrics/>
}
return (
@ -159,10 +159,15 @@ export const memoryOptions: ChartOptions = {
scales: {
yAxes: [{
ticks: {
callback: value => {
value = parseFloat(String(value));
if (!value) return 0;
return value < 1 ? value.toFixed(3) : bytesToUnits(value);
callback: (value: number | string): string => {
if (typeof value == "string") {
const float = parseFloat(value);
if (float < 1) {
return float.toFixed(3);
}
return bytesToUnits(parseInt(value));
}
return `${value}`;
},
stepSize: 1
}
@ -184,11 +189,12 @@ export const cpuOptions: ChartOptions = {
scales: {
yAxes: [{
ticks: {
callback: (value: number) => {
if (value == 0) return 0;
if (value < 10) return value.toFixed(3);
if (value < 100) return value.toFixed(2);
return value.toFixed(1);
callback: (value: number | string): string => {
const float = parseFloat(`${value}`);
if (float == 0) return "0";
if (float < 10) return float.toFixed(3);
if (float < 100) return float.toFixed(2);
return float.toFixed(1);
}
}
}]

View File

@ -1,6 +1,6 @@
import "./chart.scss";
import React from "react";
import ChartJS from "chart.js";
import ChartJS, {ChartData, ChartOptions} from "chart.js";
import { isEqual, remove } from "lodash";
import { cssNames } from "../../utils";
import { StatusBrick } from "../status-brick";
@ -17,7 +17,7 @@ export interface ChartDataSets extends ChartJS.ChartDataSets {
export interface ChartProps {
data: ChartData;
options?: ChartJS.ChartOptions; // Passed to ChartJS instance
options?: ChartOptions; // Passed to ChartJS instance
width?: number | string;
height?: number | string;
type?: ChartKind;

View File

@ -5,7 +5,10 @@ import { Chart, ChartProps } from "./chart";
import { cssNames } from "../../utils";
import { themeStore } from "../../theme.store";
export class PieChart extends React.Component<ChartProps> {
interface Props extends ChartProps {
}
export class PieChart extends React.Component<Props> {
render() {
const { data, className, options, ...chartProps } = this.props
const { contentColor } = themeStore.activeTheme.colors;

View File

@ -119,12 +119,12 @@ export class Dock extends React.Component<Props> {
/>
<div className="toolbar flex gaps align-center box grow">
<div className="dock-menu box grow">
<MenuActions usePortal triggerIcon={{ material: "add", tooltip: <Trans>New tab</Trans> }} closeOnScroll={false}>
<MenuItem onClick={() => createTerminalTab()}>
<MenuActions usePortal triggerIcon={{ material: "add", className: "new-dock-tab", tooltip: <Trans>New tab</Trans> }} closeOnScroll={false}>
<MenuItem className="create-terminal-tab" onClick={() => createTerminalTab()}>
<Icon small svg="terminal" size={15}/>
<Trans>Terminal session</Trans>
</MenuItem>
<MenuItem onClick={() => createResourceTab()}>
<MenuItem className="create-resource-tab" onClick={() => createResourceTab()}>
<Icon small material="create"/>
<Trans>Create resource</Trans>
</MenuItem>

View File

@ -1,161 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"> <image id="image0" width="512" height="512" x="0" y="0"
href="data:image/png;base64,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
rcekScnQ0DtMP5KsAL51Dg0N1g8NfavEmr2P6ZgPo1p+RMeIMDmPFdPSba/ODoD/sn8BcJ0Q6U6Z
H8IAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAASNQTFRF////
hpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCb
hpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCb
hpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCb
hpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCb
hpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCb
////f/Q5igAAAF90Uk5TAAAVQ26QsMjc7vMka6bhKITbEmwnmfcmpP2lFJX7AWPvZCC/XPEHmxjK
GSw4PjfyLfAa4xwGnL5lE+2UmvZyc9cziSOn4kWxssvf4JFEF4o02CmWXckIIsCL5BZw3fSQaiOm
AAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+QDHAczCg2f+2gAAAxo
elRYdFJhdyBwcm9maWxlIHR5cGUgaWNjAABYha2Za64jKxKE/9cqZgk8kgSWAwkpzf43MB9lu9un
7dbVlcanUdkUBUk+IoLq679m13/45Jz1CuezQtNYQw1qIcW7S5fuKjWVJFVSCqWVXkYKoW7hNl/C
DCHyPXJlIo2aa65BYgkliIXn58/ff/ncqzqrXq8ft2E5rV+W/cvP9e+Gx6iipWbNj5/l2a/pUjnd
uh43pjyue7PhUFN9/M7jcQ0p14rnwqvf2nMBuXDn7cbHjWWvG1rf+z386n8fL0HeJxIi8zBV4mOF
FowgxJq03r8tvSwKVbG/Pi0yefbncOlm113340Z93WBrSkro84H5msgI0FJ/9e+npald+jeL6t8t
1S+WXveN+nnjR3R+f5pg/8k7Zcraf9/4l+H/++f/PxEutFr+3ErS5xfTVpO0kh4/46M/rqZTXWZJ
18eNrUm2+Ed/f0bqz/5GVFopnxNRwpKfCffeLxpFZH5Z2FjYPidS7aLSPy2qIr3IRz+xlVLyx0Rk
s5M/n1tTfJQlfvST/2/Ofpuo6CzlywOsLf2FT2/jhzZSt35OxNaKyv7ozyyQP310JsLS/WVrtZRU
yqcvdB/Q/bpl/Rb+Vd9y6cfWOlv7tFTO1qR9CT9hJgpfLYrfLK25tG/hx07y5evWypcFDlpK+rK1
U9e/2ePHRCb1s0RAAiUIn5l9c5l8eYCVIajPhBy4YpTPPDooFsLZwvHHuZ45z05PxPJpjAGQQrlz
rR7cYvlf1S9ZnVU7md0BunYiLU+OI7pBgGPNaSdLTJfvdj4vBv6BR56PRb8IExIL7RdVv33UHoSW
e3oEJfV5fR2Y9p3Bs7UH37n7t3GprsdE+5nZFnoCPgpZrT9G/kag88CBFn8Kh35Y9BDmYRDaup4O
ZII8H0+eHBIeOghwolR4qPDQQU9tB0tx8ZElOLySJg1XtCMiGhMc+OlM0JmgM8HgocFDg4cmD00e
mjxk8aDuIbqjhWg8s/i+j7M3P45PDgQ5g3zi7vNXaQuXZ1qneYg4Nybus4tIGsS82Sl9p0QisY2Y
f1IJhqNxs3ITs2NjEiyOnTFYGwcLDMZOrpMrFkbjuiITLb5sOjZX5wEf5GCmHfUmtBUSbJKS49sW
EgmbwCXyn8Y9Fk+ViVLlwUZn2+QDA4lxGkwyeZgVE/5I+CKdIOGC5CWcXMxIv8w2Mloxn6hlVoE3
aB4Oe2TlJlHJdVMN/O78ZvI8+D35bRX5w/h9tIYheTrBJmqCM08ZSVrUyQyAFBkAABBuqZG2gzSj
RgZZwT0sFUMZMZngkpMl4iRkITIlWihEo+RB6nTqtYXC3gu5UpqSSrRBmwAAKFPYaiHahSjffxGL
NFGXR15giRI91RG0TnLPgvYVdPitTXXFoDsH2CqcWq6kRGXxmhcQ4BcSjIxBYFVWrp2bOLpOD3Uh
z1i1ekMXTRLYQyN/mpxSHiTzDq3xG2vbGBeJTQcPtV1DIxk7yXuqurPNjgMO53eUUG98R5R2HN6N
7yzSGT9iDCPBawPTBjkx2NKgbgYpMIjQwBcDcTr8BJo/5P/MPUwAbSpXKmgyds5TPbDIxPOTEFok
BdNAYOZgxdCTJRi5ZaMGO2VG4hqTrvNHgi45pTbCwi2rEbVFyi9jwNph4Y9NOmyybxM9hGrY1cPu
k3LUsHHB3puy7MHZvpPlDjJ4xUeOs3zm4Ozb9ylvvJSIgGTChAMqJneKa9K/6nkyUsj3vygOjK4Y
m10xDtxlPcbd2FyN1FVMUmJSiZROTCPHZLSdsZWWaCyCUAUAaIOUtnKBBIXShFOoPBQQ2n5EaROO
WaDDZlHAIaYIeUG0GuEMwGLGMldkV6fajl6+0OQ9KnCuzaPOhKXkqY9YwYYqMcJ/sbYe69iR3IrV
a2zgCHUeoc3Y2oht+BXb4gcP3pDDlrrO2Nlpn+TuWpEKiyONOHDNqBU42pHUiAN/Ynececap+YqT
GSeWzDXiQSBSIFrhWmc0/GNm0XDwArQWVbzqgbAeiXRcTLZR/Bwqr7jxye4SNwm1d49OODzv6Mp3
+n169D3SDaX4nsJLpDlJB2aAdUQK/GsXm2b5gTdtJ55KKdWE0Eqp8XV4SmtBOiOBWwmsSrlLypYS
Ry/A0pPI4thrV5Ixk6xxqisVBhelwdzFaA6DMzH0no5i10nbjfjSpKdaqTKer4uJGgnfsqdGCrWB
IUsSYJI6k+L41PtKoAnFg4G5JDRjGn2Cxg6IpDSzgsr9SpOBpEQ60GawuSlCgQltVTDT0sIvq2pa
rLw2eUnWINAThgHhNVEyiQ1eYPlI3mNytuIOcmW0jFJ8g+pdk0IglYXw4aI4uXrKxzZIPiMewH3J
DLxyBpxz9Zxny5mQCcbKEWF9Z1ktl5hzAaAL4qxYysVXPucM1GzWsbPuDlfIhXKDMLrlipxtUXKD
bVqz3KyBPuUOUmehPtm4A3dMM6pkdgqxeGbDeeq8YJiR525EtGYkVTa2ZDvnRYSplkyeZhAm73RO
ujQYCSQhPDE7953M8J0umDFTs3Ig8oAPriULlZIeRpZu7IGpsD3NIskxhXLHfFaDuWBDQcZLG5dQ
4Qc0oDI0KKBc1uZwj5pX5OGYog614aVaJ3kHxUFYTaZQx9KAR4oFWZgveA9Fz+CRqwxWwhCY3eQ+
eTDx3IlgDyGoYnMIKSWMktWJxeKshgjYui8hFx5kyQEBX4qvQ/QAdkUozgP1YAVcCBwUCrOQBCVV
yhpQB9YKYYRXx1XwWDnaXZoTZY5YiYMlBxtCXQDholCrDk5MjjbkSFCpJFi8NDKwISKbzdIjE3WO
VX0eyqtl8NgYNC9l4rXZaZwcLNNAMVtaFtau2sqyXkCmwlG07LmuAuRD4bFAAEgtjChkFrPBBuAv
dwYgjmEJl6ZuikxSMFtJLs1Qi5yYd7k443Wyh6VQtmWfdyiLs19Sqlzhda2dJGYMha3Qs7ZtHLiy
9j60+0kUjhHdLzRC1cnKc3B1x9CqNlzPe7eF0FgTzQ+nbE7428gGPO4IBQfSUWPwPLIFMVrhE8C5
VDQCAaacB4agIDOeoP7IUHm8ejLCBNig3CvOOfuqCrCrt8pB+CLPMgQDUeiujcE9jdobsmEDW6J1
jILIkQrA10kdAazVGu2QCqcVSoggxqtuIHAzAHRAwUj1jcqSipJoR1I0OKpFW4BIAOkAYBcgoTaK
FPpcjVRvsvPFgZRYjok6Ot5N51Db7j11b8zYOMsCBrt1QLt3bPXdKMs2bDSYvuHCNt2uZgpi0LmA
rsUzy73tOzi7eYY2O7oZ7YZcQ21ALajPOFqHPgDt0aHyUyxXB0yAHDRVo+iok1K8F+vUIYkDAFUU
E97sOKJzCO5tYhhJhbjr3Y0kaH2scvUJ1s/hHRrsxhjz2hcpBgt1iK/vsUmXxUZndx8ITOTtglWl
wqwKutO6XhxflMKnwZRCVVHmA1oaZbAYUgUPDJKGLNsDq0aDSJrJAGAZ0dEyNkbza0ApY1Jmc3co
3YahBJegReDyDXdsaP8kMBad0yAytqC6xkSGzLjgK4HwLV2TYEzAlx1Sp51SjnmWNtBnGF77BNjI
ujaxhtKmvBfHGVi1W4JT5hyzMNO++HbyLE7r8yieuZC27AAYB9Mc5odKOYmimTnlbBSFVpQDEF8c
0oMRAJK85DJw1oBbKzLZGdkgHKuNE4ccQeJwybI2zeAP1uoIlGb4wgALm1ONmYx9XAZjGXgGcaiB
CgjXas4GOM2sMJFt2dgMMjT74rS0gPsF3eDGvsDeBdCuwnkN/ChLFzDOfPjhvB1B7ZKbiiAm7oRr
DZ/4HvHLKcTaQH4hgrvC4LZQPMtTv5YPh6U4D0z6hfOOoW8KNLYmRuYNeG1oZIsbVQjOh7XPy13g
YpMAMCIyYNqFxiL3FwEFVMcGNUGXib+B1o3w3asbMqFt4rod5nNDRxfOE7t4RC5Ed6SMXZ5jdyDJ
BQKBd0BrsHdtV12uJ2gozcaYRtYhdSiB6iCLD5T8rNUJJ4XSLjzYfVEjW6bvZWAkZevOueu8z45v
rxNeb7qAYb1fNDjQdfes0u83EZD7Ywjb+/Hi+/cnvr0zee/7db3+duPP/h8v0H8beVt2XjfcE5Wn
AQTmYdhrD/X5fwPP/wooz5cl5fGGHn2a8+t6vXX8+KCkEGD5/fUM55r58AmsevzoQO5jcHr4CLqv
9w0G3tc1Hm9tEEfnYuHE8O1d/+vDZm/T2ni8qz1niwWYkPcl/MPn+h8EJEi9UWMHFQAACKJJREFU
eNrt3GlfVWUYRvEehBBkiEQJGUIGy8LEMMPScKqktGywefj+3yJ72S9z733Ofro8Z63/J7ge7vXG
w5FXXpEkSZIkSZIkSZIkSWOvtDRxanLq1enTM7N/Kmh25vT0q1OTpyba3q2XAM7MzS+kX65/Wpif
O/P/BLD42lL6sXq+pdcWawfw+tnp9Cv1ItNnX68YwPK58+kHqsn5c8uVAlh5YzX9OLWxemGlQgBr
6xvph6mtjfW1ngPYfHMr/Sh1sXVxs88AtnfSD1JXO9v9BbC7l36Nutvb7SmAS2+ln6LBvHWpjwDe
vpx+hwZ1+Z3hA3jXD31H2MK7wwawP5N+g4Yxsz9cAFf8Zd+Im70yTADvef+Rd/W9wQM4uJZer+Fd
Oxg0gPcP09vVh8PrgwXwgb/5HRPTHwwUgJ//jI0bgwSwm16t/ux2D2Dbz//HyN521wA2P0xvVp92
NjsGcDG9WP066hbAzY/Sg9WvmZudAvg4vVd9W+8SwIrf/xs7GysdAriQXqv+zbcPYNnvf4+h1cXW
AdxKb1UNt9oGsHY7PVU13F5rGcAn6aWq47hlAHfSQ1XHnXYB3E3vVC13WwVwLz1TtdxrFYB//2Fs
LbUJ4H56peq53yKAufRI1TPXIoAH6ZGq50GLAD5Nj1Q9h80BTKQ3qqaJxgBOpSeqps8aA5hMT1RN
k40BTKUnqqapxgA+T09UTQ8bAzhJT1RNJ40BfJGeqJq+bAzAvwc41rYaA7ianqiaZhsDSC9UXQYA
ZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcA
cAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAG
AGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBn
AHAGADeCAZT+pJ/yEjAAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgD
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
eRcblbgAAAAASUVORK5CYII=" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><style>.cls-2{fill:#3c90ce;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-2" d="M256,496C123.67,496,16,388.33,16,256S123.67,16,256,16,496,123.67,496,256,388.33,496,256,496Zm0-470C129.17,26,26,129.18,26,256S129.17,486,256,486,486,382.82,486,256,382.83,26,256,26Z"/><path class="cls-2" d="M403.22,282l65.66-81.68A220.52,220.52,0,0,0,351.61,57.81Z"/><path class="cls-2" d="M476,256a220.86,220.86,0,0,0-4-41.5L334.74,385.3H434A219,219,0,0,0,476,256Z"/><path class="cls-2" d="M247.63,121.17,139.38,69.48A220,220,0,0,0,38.81,221Z"/><path class="cls-2" d="M140.75,184.81,37.07,234.35C36.38,241.48,36,248.69,36,256A219.13,219.13,0,0,0,91,401.4Z"/><path class="cls-2" d="M210.57,396.65l63.2,78.57a219.52,219.52,0,0,0,151.38-78.57Z"/><path class="cls-2" d="M124.21,307.38l-23.91,104A219.33,219.33,0,0,0,256,476c1.26,0,2.5-.07,3.76-.1Z"/><path class="cls-2" d="M364.52,164.42,338.67,52.12A220.21,220.21,0,0,0,151.16,62.54Z"/><path class="cls-2" d="M351.71,276.74c-.46-.11-1.12-.29-1.58-.37a45.28,45.28,0,0,0-5.2-.42,50.71,50.71,0,0,1-9.73-1.6,6.13,6.13,0,0,1-2.35-2.36l-2.19-.63a70.52,70.52,0,0,0-11.31-48.85l1.93-1.73a4.3,4.3,0,0,1,1-3.08,50.88,50.88,0,0,1,8.07-5.67,43.85,43.85,0,0,0,4.51-2.63c.35-.26.83-.67,1.2-1,2.6-2.08,3.2-5.66,1.33-8s-5.49-2.57-8.09-.49c-.38.29-.88.67-1.21,1a43.42,43.42,0,0,0-3.58,3.79,51.27,51.27,0,0,1-7.32,6.63,6.1,6.1,0,0,1-3.3.36l-2.06,1.47a71.09,71.09,0,0,0-45.06-21.77c-.05-.72-.11-2-.12-2.42a4.33,4.33,0,0,1-1.78-2.72,50.16,50.16,0,0,1,.62-9.84,45.29,45.29,0,0,0,.74-5.17c0-.43,0-1.07,0-1.54,0-3.33-2.43-6-5.43-6s-5.43,2.7-5.43,6c0,.05,0,.1,0,.15,0,.45,0,1,0,1.39a45.29,45.29,0,0,0,.74,5.17,50.87,50.87,0,0,1,.61,9.84,5.9,5.9,0,0,1-1.77,2.81l-.12,2.29a71.38,71.38,0,0,0-9.82,1.51,69.83,69.83,0,0,0-35.46,20.26c-.6-.41-1.65-1.16-2-1.39a4.32,4.32,0,0,1-3.23-.31,50.46,50.46,0,0,1-7.31-6.61,45,45,0,0,0-3.58-3.79c-.33-.29-.83-.67-1.2-1a6.42,6.42,0,0,0-3.78-1.42,5.22,5.22,0,0,0-4.33,1.91c-1.87,2.34-1.27,5.93,1.33,8l.09.06c.35.29.79.66,1.12.91a46.81,46.81,0,0,0,4.5,2.63,49.61,49.61,0,0,1,8.07,5.67,6,6,0,0,1,1.09,3.13l1.74,1.55a70.25,70.25,0,0,0-11.07,49l-2.28.66a7.61,7.61,0,0,1-2.33,2.36,51.5,51.5,0,0,1-9.73,1.6,45.38,45.38,0,0,0-5.21.41c-.41.08-1,.23-1.45.34h0l-.08,0c-3.2.78-5.26,3.72-4.6,6.61a5.75,5.75,0,0,0,7,4h.08l.1,0c.45-.1,1-.21,1.41-.32a45.65,45.65,0,0,0,4.87-1.87,51.49,51.49,0,0,1,9.46-2.78,6.07,6.07,0,0,1,3.12,1.1l2.37-.4A70.59,70.59,0,0,0,225,322.21l-1,2.37a5.46,5.46,0,0,1,.48,3.07,53.5,53.5,0,0,1-4.92,8.83,46,46,0,0,0-2.91,4.33c-.21.41-.48,1-.69,1.47a5.73,5.73,0,0,0,2.31,7.71c2.69,1.3,6-.07,7.49-3.06v0h0c.21-.42.5-1,.67-1.38a45,45,0,0,0,1.57-5c1.43-3.61,2.22-7.4,4.2-9.75a4.31,4.31,0,0,1,2.34-1.14l1.23-2.23a70.15,70.15,0,0,0,40.78,2.93,71.11,71.11,0,0,0,9.31-2.8l1.16,2.09a4.26,4.26,0,0,1,2.77,1.68,50.16,50.16,0,0,1,3.72,9.12,44.6,44.6,0,0,0,1.58,5c.18.4.47,1,.67,1.39,1.45,3,4.81,4.38,7.51,3.07s3.7-4.72,2.3-7.71c-.2-.43-.49-1.05-.7-1.46A45.8,45.8,0,0,0,302,336.4a51.18,51.18,0,0,1-4.82-8.62,4.27,4.27,0,0,1,.42-3.2,20.73,20.73,0,0,1-.9-2.19A70.66,70.66,0,0,0,328,283c.7.11,1.92.33,2.32.41a4.28,4.28,0,0,1,3-1.13,50.68,50.68,0,0,1,9.47,2.79,45.66,45.66,0,0,0,4.87,1.88c.39.1,1,.2,1.4.3l.11,0h.08a5.72,5.72,0,0,0,7-4C357,280.45,354.91,277.51,351.71,276.74Zm-51.57-55.29-23.2,16.45-.08,0a4.79,4.79,0,0,1-6.56-.88,4.71,4.71,0,0,1-1.05-2.77h0l-1.61-28.43A56.44,56.44,0,0,1,300.14,221.45Zm-43.7,31.16h8.73l5.43,6.79-2,8.46-7.84,3.77L253,267.85,251,259.39Zm-8.22-45.84a56.58,56.58,0,0,1,5.79-1l-1.61,28.47-.12.06a4.79,4.79,0,0,1-7.59,3.67l-.05,0-23.35-16.55A55.92,55.92,0,0,1,248.22,206.77Zm-35.39,25.3,21.32,19.07,0,.12a4.79,4.79,0,0,1-1.88,8.22l0,.09-27.32,7.89A55.83,55.83,0,0,1,212.83,232.07Zm28.54,50.65L230.51,309A56.27,56.27,0,0,1,208,280.73L236,276l0,.06A4.37,4.37,0,0,1,237,276a4.78,4.78,0,0,1,4.33,6.67Zm32,33.74a56.07,56.07,0,0,1-30.62-1.58L256.56,290h0a4.77,4.77,0,0,1,4-2.53A4.86,4.86,0,0,1,265,290h.1l13.82,25Q276.24,315.82,273.41,316.46Zm17.81-7.4-11-26.5,0-.05a4.78,4.78,0,0,1,2.32-6.2,4.65,4.65,0,0,1,1.83-.48,5.05,5.05,0,0,1,1.1.08l.05-.05,28.26,4.77A56,56,0,0,1,291.22,309.06Zm25.59-41.69-27.46-7.91,0-.12a4.79,4.79,0,0,1-3.4-5.68,4.7,4.7,0,0,1,1.52-2.54v-.06l21.2-19a56.92,56.92,0,0,1,8.17,35.28Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -117,12 +117,11 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
try {
await Promise.all(stores.map(store => store.loadAll()));
const subscriptions = stores.map(store => store.subscribe());
await when(() => this.isUnmounting);
subscriptions.forEach(dispose => dispose()); // unsubscribe all
} catch(error) {
console.log("catched", error)
}
await when(() => this.isUnmounting);
}
componentWillUnmount() {

View File

@ -226,7 +226,7 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
}
render() {
const { isHidden, subMenus = [], icon, text, url, children, className } = this.props;
const { id, isHidden, subMenus = [], icon, text, url, children, className } = this.props;
if (isHidden) {
return null;
}
@ -234,7 +234,7 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
if (extendedView) {
const isActive = this.isActive();
return (
<div className={cssNames("SidebarNavItem", className)}>
<div id={id} className={cssNames("SidebarNavItem", className)}>
<div className={cssNames("nav-item", { active: isActive })} onClick={this.toggleSubMenu}>
{icon}
<span className="link-text">{text}</span>

View File

View File

@ -2,15 +2,25 @@
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where your might need to do something to ensure the application works smoothly. So please read through the release highlights!
## 3.5.0-beta.1 (current version)
## 3.5.0 (current version)
- Dynamic dashboard UI based on RBAC rules
- Dynamic dashboard UI based on RBAC rules (hides non-accessible menus)
- Show object reference for all objects
- Unify scrollbars/paddings
- New logo
- Remove Helm release update checker
- Improve Helm release version detection
- Show owner reference on all resource details
- Fix: add arch node selector for hybrid clusters
- Fix pod shell command on Windows
- Fix app freeze after closing terminal on Windows
- Fix: use correct kubeconfig context on terminal when switching cluster
- Fix error when closing Lens on Windows
- Fix: deploy kube-state-metrics component only to amd64 nodes
- Translation correction: transit to transmit
- Remove Kontena reference from Lens logo
- Track telemetry pref changed event
- Integration tests using spectron
## 3.4.0
@ -280,4 +290,3 @@ Here you can find description of changes we've built into each release. While we
## 2.0.0
Initial release of the Lens desktop application. Basic functionality with auto-import of users local kubeconfig for cluster access.