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

Merge branch 'master' into fix/add-analytics-node-types

This commit is contained in:
Lauri Nevala 2020-11-02 20:58:48 +02:00 committed by GitHub
commit c1ab1423bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 3994 additions and 78 deletions

17
.github/labeler-config.yml vendored Normal file
View File

@ -0,0 +1,17 @@
---
area/ui:
- src/renderer/**/*
area/test:
- integration/**/*
- __mocks__/**/*
area/extension:
- extensions/**/*
- src/extensions/**/*
area/documentation:
- README.md
- docs/**/*
area/ci:
- .github/workflows/**/*
- .azure-pipelines.yml
dependencies:
- yarn.lock

14
.github/workflows/labeler.yml vendored Normal file
View File

@ -0,0 +1,14 @@
---
name: "Pull Request Labeler"
'on':
- pull_request
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/labeler-config.yml

View File

@ -0,0 +1,5 @@
install-deps:
npm install
build: install-deps
npm run build

View File

@ -0,0 +1,13 @@
import { LensMainExtension, Util } from "@k8slens/extensions";
export default class LicenseLensMainExtension extends LensMainExtension {
appMenus = [
{
parentId: "help",
label: "License",
async click() {
Util.openExternal("https://k8slens.dev/licenses/eula.md")
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
{
"name": "lens-license",
"version": "0.1.0",
"description": "License menu item",
"main": "dist/main.js",
"scripts": {
"build": "webpack -p",
"dev": "webpack --watch"
},
"dependencies": {},
"devDependencies": {
"@types/webpack": "^4.41.17",
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"mobx": "^5.15.5",
"react": "^16.13.1",
"ts-loader": "^8.0.4",
"ts-node": "^9.0.0",
"typescript": "^4.0.3",
"webpack": "^4.44.2"
}
}

View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"outDir": "dist",
"baseUrl": ".",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react"
}
}

View File

@ -0,0 +1,34 @@
import path from "path"
const outputPath = path.resolve(__dirname, 'dist');
export default [
{
entry: './main.ts',
context: __dirname,
target: "electron-main",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
externals: {
"@k8slens/extensions": "var global.LensExtensions",
"mobx": "var global.Mobx",
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: 'main.js',
path: outputPath,
},
},
];

View File

@ -22,8 +22,7 @@ export default class SupportPageRendererExtension extends LensRendererExtension
className="flex align-center gaps hover-highlight" className="flex align-center gaps hover-highlight"
onClick={() => Navigation.navigate(supportPageURL())} onClick={() => Navigation.navigate(supportPageURL())}
> >
<Component.Icon material="help_outline" small/> <Component.Icon material="help" smallest />
<span>Support</span>
</div> </div>
) )
} }

View File

@ -7,6 +7,7 @@ export default class TelemetryMainExtension extends LensMainExtension {
async onActivate() { async onActivate() {
console.log("telemetry main extension activated") console.log("telemetry main extension activated")
tracker.start() tracker.start()
tracker.reportPeriodically()
await telemetryPreferencesStore.loadExtension(this) await telemetryPreferencesStore.loadExtension(this)
} }

View File

@ -13,6 +13,16 @@
"resolved": "https://registry.npmjs.org/@types/analytics-node/-/analytics-node-3.1.3.tgz", "resolved": "https://registry.npmjs.org/@types/analytics-node/-/analytics-node-3.1.3.tgz",
"integrity": "sha512-Yk299LUqnyJ6fNYQkLFd0yTfUwIvgfxH3f5WEX3ib0PC5T+mZgqcOPMDhNZ4AOD/A9tXKJQeBIb6KvgzuXflaQ==", "integrity": "sha512-Yk299LUqnyJ6fNYQkLFd0yTfUwIvgfxH3f5WEX3ib0PC5T+mZgqcOPMDhNZ4AOD/A9tXKJQeBIb6KvgzuXflaQ==",
"dev": true "dev": true
}
"@segment/loosely-validate-event": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz",
"integrity": "sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==",
"dev": true,
"requires": {
"component-type": "^1.2.1",
"join-component": "^1.1.0"
}
}, },
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.9.0", "version": "1.9.0",
@ -231,6 +241,22 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true "dev": true
}, },
"analytics-node": {
"version": "3.4.0-beta.3",
"resolved": "https://registry.npmjs.org/analytics-node/-/analytics-node-3.4.0-beta.3.tgz",
"integrity": "sha512-NIdpxiwlZ4cKgs9MDlDe89b5bg/pMq2W7XTA+cjzCM66IwW3ujZhVE49vk+zG6Yrxk0s/DXmennJ+cCQIsTKMA==",
"dev": true,
"requires": {
"@segment/loosely-validate-event": "^2.0.0",
"axios": "^0.19.2",
"axios-retry": "^3.0.2",
"lodash.isstring": "^4.0.1",
"md5": "^2.2.1",
"ms": "^2.0.0",
"remove-trailing-slash": "^0.1.0",
"uuid": "^3.2.1"
}
},
"ansi-styles": { "ansi-styles": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@ -380,6 +406,24 @@
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
"dev": true "dev": true
}, },
"axios": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"dev": true,
"requires": {
"follow-redirects": "1.5.10"
}
},
"axios-retry": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.1.9.tgz",
"integrity": "sha512-NFCoNIHq8lYkJa6ku4m+V1837TP6lCa7n79Iuf8/AqATAHYB0ISaAS1eyIenDOfHOLtym34W65Sjke2xjg2fsA==",
"dev": true,
"requires": {
"is-retry-allowed": "^1.1.0"
}
},
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@ -702,6 +746,12 @@
"supports-color": "^5.3.0" "supports-color": "^5.3.0"
} }
}, },
"charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=",
"dev": true
},
"chokidar": { "chokidar": {
"version": "3.4.2", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz",
@ -819,6 +869,12 @@
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
"dev": true "dev": true
}, },
"component-type": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-type/-/component-type-1.2.1.tgz",
"integrity": "sha1-ikeQFwAjjk/DIml3EjAibyS0Fak=",
"dev": true
},
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -920,6 +976,12 @@
"sha.js": "^2.4.8" "sha.js": "^2.4.8"
} }
}, },
"crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=",
"dev": true
},
"crypto-browserify": { "crypto-browserify": {
"version": "3.12.0", "version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@ -1383,6 +1445,26 @@
"readable-stream": "^2.3.6" "readable-stream": "^2.3.6"
} }
}, },
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"dev": true,
"requires": {
"debug": "=3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
}
}
},
"for-in": { "for-in": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@ -1790,6 +1872,12 @@
"isobject": "^3.0.1" "isobject": "^3.0.1"
} }
}, },
"is-retry-allowed": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz",
"integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==",
"dev": true
},
"is-typedarray": { "is-typedarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
@ -1826,6 +1914,12 @@
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
"dev": true "dev": true
}, },
"join-component": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/join-component/-/join-component-1.1.0.tgz",
"integrity": "sha1-uEF7dQZho5K+4sJTfGiyqdSXfNU=",
"dev": true
},
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -1916,6 +2010,12 @@
"path-exists": "^3.0.0" "path-exists": "^3.0.0"
} }
}, },
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=",
"dev": true
},
"loose-envify": { "loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -1967,6 +2067,17 @@
"object-visit": "^1.0.0" "object-visit": "^1.0.0"
} }
}, },
"md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"dev": true,
"requires": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "~1.1.6"
}
},
"md5.js": { "md5.js": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -2621,6 +2732,12 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"remove-trailing-slash": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz",
"integrity": "sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==",
"dev": true
},
"repeat-element": { "repeat-element": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",

View File

@ -22,6 +22,7 @@
"mobx": "^5.15.5", "mobx": "^5.15.5",
"react": "^16.13.1", "react": "^16.13.1",
"node-machine-id": "^1.1.12", "node-machine-id": "^1.1.12",
"universal-analytics": "^0.4.23" "universal-analytics": "^0.4.23",
"analytics-node": "^3.4.0-beta.3"
} }
} }

View File

@ -1,31 +1,40 @@
import { EventBus, Util, Store, App } from "@k8slens/extensions" import { EventBus, Util, Store, App } from "@k8slens/extensions"
import ua from "universal-analytics" import ua from "universal-analytics"
import Analytics from "analytics-node"
import { machineIdSync } from "node-machine-id" import { machineIdSync } from "node-machine-id"
import { telemetryPreferencesStore } from "./telemetry-preferences-store" import { telemetryPreferencesStore } from "./telemetry-preferences-store"
export class Tracker extends Util.Singleton { export class Tracker extends Util.Singleton {
static readonly GA_ID = "UA-159377374-1" static readonly GA_ID = "UA-159377374-1"
static readonly SEGMENT_KEY = "YENwswyhlOgz8P7EFKUtIZ2MfON7Yxqb"
protected eventHandlers: Array<(ev: EventBus.AppEvent ) => void> = [] protected eventHandlers: Array<(ev: EventBus.AppEvent ) => void> = []
protected started = false protected started = false
protected visitor: ua.Visitor protected visitor: ua.Visitor
protected analytics: Analytics
protected machineId: string = null; protected machineId: string = null;
protected ip: string = null; protected ip: string = null;
protected appVersion: string; protected appVersion: string;
protected locale: string; protected locale: string;
protected electronUA: string; protected userAgent: string;
protected anonymousId: string;
protected os: string
protected reportInterval: NodeJS.Timeout protected reportInterval: NodeJS.Timeout
private constructor() { private constructor() {
super(); super();
this.anonymousId = machineIdSync()
this.os = this.resolveOS()
this.userAgent = `Lens ${App.version} (${this.os})`
try { try {
this.visitor = ua(Tracker.GA_ID, machineIdSync(), { strictCidFormat: false }) this.visitor = ua(Tracker.GA_ID, this.anonymousId, { strictCidFormat: false })
} catch (error) { } catch (error) {
this.visitor = ua(Tracker.GA_ID) this.visitor = ua(Tracker.GA_ID)
} }
this.analytics = new Analytics(Tracker.SEGMENT_KEY, { flushAt: 1 })
this.visitor.set("dl", "https://telemetry.k8slens.dev") this.visitor.set("dl", "https://telemetry.k8slens.dev")
this.visitor.set("ua", `Lens ${App.version} (${this.getOS()})`) this.visitor.set("ua", this.userAgent)
} }
start() { start() {
@ -38,6 +47,9 @@ export class Tracker extends Util.Singleton {
} }
this.eventHandlers.push(handler) this.eventHandlers.push(handler)
EventBus.appEventBus.addListener(handler) EventBus.appEventBus.addListener(handler)
}
reportPeriodically() {
this.reportInterval = setInterval(() => { this.reportInterval = setInterval(() => {
this.reportData() this.reportData()
}, 60 * 60 * 1000) // report every 1h }, 60 * 60 * 1000) // report every 1h
@ -61,12 +73,13 @@ export class Tracker extends Util.Singleton {
} }
protected reportData() { protected reportData() {
const clustersList = Store.clusterStore.clustersList const clustersList = Store.clusterStore.enabledClustersList
this.event("generic-data", "report", { this.event("generic-data", "report", {
appVersion: App.version, appVersion: App.version,
os: this.os,
clustersCount: clustersList.length, clustersCount: clustersList.length,
workspacesCount: Store.workspaceStore.workspacesList.length workspacesCount: Store.workspaceStore.enabledWorkspacesList.length
}) })
clustersList.forEach((cluster) => { clustersList.forEach((cluster) => {
@ -78,6 +91,7 @@ export class Tracker extends Util.Singleton {
protected reportClusterData(cluster: Store.ClusterModel) { protected reportClusterData(cluster: Store.ClusterModel) {
this.event("cluster-data", "report", { this.event("cluster-data", "report", {
id: cluster.metadata.id, id: cluster.metadata.id,
managed: !!cluster.ownerRef,
kubernetesVersion: cluster.metadata.version, kubernetesVersion: cluster.metadata.version,
distribution: cluster.metadata.distribution, distribution: cluster.metadata.distribution,
nodesCount: cluster.metadata.nodes, nodesCount: cluster.metadata.nodes,
@ -85,7 +99,7 @@ export class Tracker extends Util.Singleton {
}) })
} }
protected getOS() { protected resolveOS() {
let os = "" let os = ""
if (App.isMac) { if (App.isMac) {
os = "MacOS" os = "MacOS"
@ -115,6 +129,19 @@ export class Tracker extends Util.Singleton {
ea: eventAction, ea: eventAction,
...otherParams, ...otherParams,
}).send() }).send()
this.analytics.track({
anonymousId: this.anonymousId,
event: `${eventCategory} ${eventAction}`,
context: {
userAgent: this.userAgent,
},
properties: {
category: eventCategory,
...otherParams,
},
})
} catch (err) { } catch (err) {
console.error(`Failed to track "${eventCategory}:${eventAction}"`, err) console.error(`Failed to track "${eventCategory}:${eventAction}"`, err)
} }

View File

@ -185,6 +185,7 @@
"pod-menu", "pod-menu",
"node-menu", "node-menu",
"metrics-cluster-feature", "metrics-cluster-feature",
"license-menu-item",
"support-page" "support-page"
] ]
}, },

View File

@ -11,4 +11,4 @@ export * from "./getRandId"
export * from "./splitArray" export * from "./splitArray"
export * from "./saveToAppFiles" export * from "./saveToAppFiles"
export * from "./singleton" export * from "./singleton"
export * from "./cloneJson" export * from "./openExternal"

View File

@ -0,0 +1,6 @@
// Opens a link in external browser
import { shell } from "electron"
export function openExternal(url: string) {
return shell.openExternal(url);
}

View File

@ -1,3 +1,3 @@
export { Singleton } from "../../common/utils" export { Singleton, openExternal } from "../../common/utils"
export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault" export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault"
export { cssNames } from "../../renderer/utils/cssNames" export { cssNames } from "../../renderer/utils/cssNames"

View File

@ -6,7 +6,10 @@ import { broadcastIpc } from "../common/ipc"
import { observable, reaction, toJS, } from "mobx" import { observable, reaction, toJS, } from "mobx"
import logger from "../main/logger" import logger from "../main/logger"
import { app, ipcRenderer, remote } from "electron" import { app, ipcRenderer, remote } from "electron"
import { appPreferenceRegistry, clusterFeatureRegistry, clusterPageRegistry, globalPageRegistry, kubeObjectMenuRegistry, menuRegistry, statusBarRegistry } from "./registries"; import {
appPreferenceRegistry, clusterFeatureRegistry, clusterPageRegistry, globalPageRegistry,
kubeObjectDetailRegistry, kubeObjectMenuRegistry, menuRegistry, statusBarRegistry
} from "./registries";
export interface InstalledExtension extends ExtensionModel { export interface InstalledExtension extends ExtensionModel {
manifestPath: string; manifestPath: string;
@ -56,6 +59,7 @@ export class ExtensionLoader {
this.autoloadExtensions((extension: LensRendererExtension) => { this.autoloadExtensions((extension: LensRendererExtension) => {
extension.registerTo(clusterPageRegistry, extension.clusterPages) extension.registerTo(clusterPageRegistry, extension.clusterPages)
extension.registerTo(kubeObjectMenuRegistry, extension.kubeObjectMenuItems) extension.registerTo(kubeObjectMenuRegistry, extension.kubeObjectMenuItems)
extension.registerTo(kubeObjectDetailRegistry, extension.kubeObjectDetailItems)
}) })
} }

View File

@ -1,4 +1,8 @@
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectMenuRegistration, PageRegistration, StatusBarRegistration } from "./registries" import type {
AppPreferenceRegistration, ClusterFeatureRegistration,
KubeObjectMenuRegistration, KubeObjectDetailRegistration,
PageRegistration, StatusBarRegistration
} from "./registries"
import { observable } from "mobx"; import { observable } from "mobx";
import { LensExtension } from "./lens-extension" import { LensExtension } from "./lens-extension"
@ -8,5 +12,6 @@ export class LensRendererExtension extends LensExtension {
@observable.shallow appPreferences: AppPreferenceRegistration[] = [] @observable.shallow appPreferences: AppPreferenceRegistration[] = []
@observable.shallow clusterFeatures: ClusterFeatureRegistration[] = [] @observable.shallow clusterFeatures: ClusterFeatureRegistration[] = []
@observable.shallow statusBarItems: StatusBarRegistration[] = [] @observable.shallow statusBarItems: StatusBarRegistration[] = []
@observable.shallow kubeObjectDetailItems: KubeObjectDetailRegistration[] = []
@observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = [] @observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = []
} }

View File

@ -4,5 +4,6 @@ export * from "./page-registry"
export * from "./menu-registry" export * from "./menu-registry"
export * from "./app-preference-registry" export * from "./app-preference-registry"
export * from "./status-bar-registry" export * from "./status-bar-registry"
export * from "./kube-object-detail-registry";
export * from "./kube-object-menu-registry"; export * from "./kube-object-menu-registry";
export * from "./cluster-feature-registry" export * from "./cluster-feature-registry"

View File

@ -0,0 +1,22 @@
import React from "react"
import { BaseRegistry } from "./base-registry";
export interface KubeObjectDetailComponents {
Details: React.ComponentType<any>;
}
export interface KubeObjectDetailRegistration {
kind: string;
apiVersions: string[];
components: KubeObjectDetailComponents;
}
export class KubeObjectDetailRegistry extends BaseRegistry<KubeObjectDetailRegistration> {
getItemsForKind(kind: string, apiVersion: string) {
return this.items.filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion)
})
}
}
export const kubeObjectDetailRegistry = new KubeObjectDetailRegistry()

View File

@ -11,7 +11,8 @@ export * from "../../renderer/components/drawer"
// kube helpers // kube helpers
export { KubeObjectDetailsProps, KubeObjectMenuProps } from "../../renderer/components/kube-object" export { KubeObjectDetailsProps, KubeObjectMenuProps } from "../../renderer/components/kube-object"
export { KubeObjectMeta } from "../../renderer/components/kube-object/kube-object-meta"; export { KubeObjectMeta } from "../../renderer/components/kube-object/kube-object-meta"
export { KubeObjectListLayout, KubeObjectListLayoutProps } from "../../renderer/components/kube-object/kube-object-list-layout";
export { KubeEventDetails } from "../../renderer/components/+events/kube-event-details" export { KubeEventDetails } from "../../renderer/components/+events/kube-event-details"
// specific exports // specific exports

View File

@ -1,4 +1,6 @@
export { isAllowedResource } from "../../common/rbac"
export { apiManager } from "../../renderer/api/api-manager"; export { apiManager } from "../../renderer/api/api-manager";
export { KubeObjectStore } from "../../renderer/kube-object.store"
export { KubeApi, forCluster, IKubeApiCluster } from "../../renderer/api/kube-api"; export { KubeApi, forCluster, IKubeApiCluster } from "../../renderer/api/kube-api";
export { KubeObject } from "../../renderer/api/kube-object"; export { KubeObject } from "../../renderer/api/kube-object";
export { Pod, podsApi, IPodContainer, IPodContainerStatus } from "../../renderer/api/endpoints"; export { Pod, podsApi, IPodContainer, IPodContainerStatus } from "../../renderer/api/endpoints";

View File

@ -1 +1,3 @@
export { navigate, hideDetails, showDetails } from "../../renderer/navigation" export { navigate, hideDetails, showDetails, getDetailsUrl } from "../../renderer/navigation"
export { RouteProps } from "react-router"
export { IURLParams } from "../../common/utils/buildUrl";

View File

@ -31,7 +31,7 @@ export class DistributionDetector extends BaseClusterDetector {
if (this.isCustom()) { if (this.isCustom()) {
return { value: "custom", accuracy: 10} return { value: "custom", accuracy: 10}
} }
return { value: "vanilla", accuracy: 10} return { value: "unknown", accuracy: 10}
} }
public async getKubernetesVersion() { public async getKubernetesVersion() {

View File

@ -5,13 +5,12 @@ export class NodesCountDetector extends BaseClusterDetector {
key = ClusterMetadataKey.NODES_COUNT key = ClusterMetadataKey.NODES_COUNT
public async detect() { public async detect() {
if (!this.cluster.accessible) return null;
const nodeCount = await this.getNodeCount() const nodeCount = await this.getNodeCount()
return { value: nodeCount, accuracy: 100} return { value: nodeCount, accuracy: 100}
} }
protected async getNodeCount(): Promise<number> { protected async getNodeCount(): Promise<number> {
if (!this.cluster.accessible) return null;
const response = await this.k8sRequest("/api/v1/nodes") const response = await this.k8sRequest("/api/v1/nodes")
return response.items.length return response.items.length
} }

View File

@ -1,5 +1,6 @@
import "../common/cluster-ipc"; import "../common/cluster-ipc";
import type http from "http" import type http from "http"
import { ipcMain } from "electron"
import { autorun } from "mobx"; import { autorun } from "mobx";
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store" import { clusterStore, getClusterIdFromHost } from "../common/cluster-store"
import { Cluster } from "./cluster" import { Cluster } from "./cluster"
@ -30,6 +31,29 @@ export class ClusterManager {
}, { }, {
delay: 250 delay: 250
}); });
ipcMain.on("network:offline", () => { this.onNetworkOffline() })
ipcMain.on("network:online", () => { this.onNetworkOnline() })
}
protected onNetworkOffline() {
logger.info("[CLUSTER-MANAGER]: network is offline")
clusterStore.enabledClustersList.forEach((cluster) => {
if (!cluster.disconnected) {
cluster.online = false
cluster.accessible = false
cluster.refreshConnectionStatus().catch((e) => e)
}
})
}
protected onNetworkOnline() {
logger.info("[CLUSTER-MANAGER]: network is online")
clusterStore.enabledClustersList.forEach((cluster) => {
if (!cluster.disconnected) {
cluster.refreshConnectionStatus().catch((e) => e)
}
})
} }
stop() { stop() {

View File

@ -67,12 +67,12 @@ export class Cluster implements ClusterModel, ClusterState {
@observable kubeConfigPath: string; @observable kubeConfigPath: string;
@observable apiUrl: string; // cluster server url @observable apiUrl: string; // cluster server url
@observable kubeProxyUrl: string; // lens-proxy to kube-api url @observable kubeProxyUrl: string; // lens-proxy to kube-api url
@observable enabled = false; @observable enabled = false; // only enabled clusters are visible to users
@observable online = false; @observable online = false; // describes if we can detect that cluster is online
@observable accessible = false; @observable accessible = false; // if user is able to access cluster resources
@observable ready = false; @observable ready = false; // cluster is in usable state
@observable reconnecting = false; @observable reconnecting = false;
@observable disconnected = true; @observable disconnected = true; // false if user has selected to connect
@observable failureReason: string; @observable failureReason: string;
@observable isAdmin = false; @observable isAdmin = false;
@observable eventCount = 0; @observable eventCount = 0;
@ -127,16 +127,16 @@ export class Cluster implements ClusterModel, ClusterState {
} }
protected bindEvents() { protected bindEvents() {
logger.info(`[CLUSTER]: bind events`, this.getMeta()); logger.info(`[CLUSTER]: bind events`, this.getMeta())
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000) // every 30s
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000) // every 15 minutes
if (ipcMain) { if (ipcMain) {
this.eventDisposers.push( this.eventDisposers.push(
reaction(() => this.getState(), () => this.pushState()), reaction(() => this.getState(), () => this.pushState()),
() => { () => {
clearInterval(refreshTimer); clearInterval(refreshTimer)
clearInterval(refreshMetadataTimer); clearInterval(refreshMetadataTimer)
}, },
); );
} }

View File

@ -82,6 +82,12 @@ export class LensProxy {
proxySocket.write("\r\n") proxySocket.write("\r\n")
proxySocket.write(head) proxySocket.write(head)
}) })
proxySocket.setKeepAlive(true)
socket.setKeepAlive(true)
proxySocket.setTimeout(0)
socket.setTimeout(0)
proxySocket.on('data', function (chunk) { proxySocket.on('data', function (chunk) {
socket.write(chunk) socket.write(chunk)
}) })

View File

@ -1,4 +1,4 @@
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron" import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, webContents } from "electron"
import { autorun } from "mobx"; import { autorun } from "mobx";
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { appName, isMac, isWindows } from "../common/vars"; import { appName, isMac, isWindows } from "../common/vars";
@ -185,12 +185,6 @@ export function buildMenu(windowManager: WindowManager) {
navigate(whatsNewURL()) navigate(whatsNewURL())
}, },
}, },
{
label: "License",
click: async () => {
shell.openExternal('https://k8slens.dev/licenses/eula.md');
},
},
...ignoreOnMac([ ...ignoreOnMac([
{ {
label: "About Lens", label: "About Lens",

View File

@ -1,33 +1 @@
import { observable } from "mobx" export { kubeObjectDetailRegistry } from "../../extensions/registries/kube-object-detail-registry"
import React from "react"
export interface KubeObjectDetailComponents {
Details: React.ComponentType<any>;
}
export interface KubeObjectDetailRegistration {
kind: string;
apiVersions: string[];
components: KubeObjectDetailComponents;
}
export class KubeObjectDetailRegistry {
items = observable.array<KubeObjectDetailRegistration>([], { deep: false });
add(item: KubeObjectDetailRegistration) {
this.items.push(item)
return () => {
this.items.replace(
this.items.filter(c => c !== item)
)
};
}
getItemsForKind(kind: string, apiVersion: string) {
return this.items.filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion)
})
}
}
export const kubeObjectDetailRegistry = new KubeObjectDetailRegistry()

View File

@ -109,17 +109,22 @@ export class KubeWatchApi {
} }
} }
protected async onRouteEvent({ type, url }: IKubeWatchRouteEvent) { protected async onRouteEvent(event: IKubeWatchRouteEvent) {
if (type === "STREAM_END") { if (event.type === "STREAM_END") {
this.disconnect(); this.disconnect();
const { apiBase, namespace } = KubeApi.parseApi(url); const { apiBase, namespace } = KubeApi.parseApi(event.url);
const api = apiManager.getApi(apiBase); const api = apiManager.getApi(apiBase);
if (api) { if (api) {
try { try {
await api.refreshResourceVersion({ namespace }); await api.refreshResourceVersion({ namespace });
this.reconnect(); this.reconnect();
} catch (error) { } catch (error) {
console.debug("failed to refresh resource version", error) console.error("failed to refresh resource version", error)
if (this.subscribers.size > 0) {
setTimeout(() => {
this.onRouteEvent(event)
}, 1000)
}
} }
} }
} }

View File

@ -15,7 +15,7 @@ export interface AnimateProps {
@observer @observer
export class Animate extends React.Component<AnimateProps> { export class Animate extends React.Component<AnimateProps> {
static VISIBILITY_DELAY_MS = 100; static VISIBILITY_DELAY_MS = 0;
static defaultProps: AnimateProps = { static defaultProps: AnimateProps = {
name: "opacity", name: "opacity",

View File

@ -54,6 +54,9 @@ export class App extends React.Component {
appEventBus.emit({name: "cluster", action: "open", params: { appEventBus.emit({name: "cluster", action: "open", params: {
clusterId: clusterId clusterId: clusterId
}}) }})
window.addEventListener("online", () => {
window.location.reload()
})
} }
get startURL() { get startURL() {

View File

@ -4,8 +4,9 @@
font-size: $font-size-small; font-size: $font-size-small;
background-color: #3d90ce; background-color: #3d90ce;
padding: 0 $padding; padding: 0 2px;
color: white; color: white;
height: 22px;
#current-workspace { #current-workspace {
padding: $padding / 4 $padding / 2; padding: $padding / 4 $padding / 2;

View File

@ -14,7 +14,7 @@ export class BottomBar extends React.Component {
return ( return (
<div className="BottomBar flex gaps"> <div className="BottomBar flex gaps">
<div id="current-workspace" className="flex gaps align-center hover-highlight"> <div id="current-workspace" className="flex gaps align-center hover-highlight">
<Icon small material="layers"/> <Icon smallest material="layers"/>
<span className="workspace-name">{currentWorkspace.name}</span> <span className="workspace-name">{currentWorkspace.name}</span>
</div> </div>
<WorkspaceMenu <WorkspaceMenu

View File

@ -1,6 +1,7 @@
.Icon { .Icon {
--size: 21px; --size: 21px;
--small-size: 18px; --small-size: 18px;
--smallest-size: 16px;
--big-size: 32px; --big-size: 32px;
--color-active: #{$iconActiveColor}; --color-active: #{$iconActiveColor};
--bgc-active: #{$iconActiveBackground}; --bgc-active: #{$iconActiveBackground};
@ -21,6 +22,12 @@
width: var(--size); width: var(--size);
height: var(--size); height: var(--size);
&.smallest {
font-size: var(--smallest-size);
width: var(--smallest-size);
height: var(--smallest-size);
}
&.small { &.small {
font-size: var(--small-size); font-size: var(--small-size);
width: var(--small-size); width: var(--small-size);

View File

@ -15,6 +15,7 @@ export interface IconProps extends React.HTMLAttributes<any>, TooltipDecoratorPr
href?: string; // render icon as hyperlink href?: string; // render icon as hyperlink
size?: string | number; // icon-size size?: string | number; // icon-size
small?: boolean; // pre-defined icon-size small?: boolean; // pre-defined icon-size
smallest?: boolean; // pre-defined icon-size
big?: boolean; // pre-defined icon-size big?: boolean; // pre-defined icon-size
active?: boolean; // apply active-state styles active?: boolean; // apply active-state styles
interactive?: boolean; // indicates that icon is interactive and highlight it on focus/hover interactive?: boolean; // indicates that icon is interactive and highlight it on focus/hover
@ -63,7 +64,7 @@ export class Icon extends React.PureComponent<IconProps> {
const { isInteractive } = this; const { isInteractive } = this;
const { const {
// skip passing props to icon's html element // skip passing props to icon's html element
className, href, link, material, svg, size, small, big, className, href, link, material, svg, size, smallest, small, big,
disabled, sticker, active, focusable, children, disabled, sticker, active, focusable, children,
interactive: _interactive, interactive: _interactive,
onClick: _onClick, onClick: _onClick,
@ -75,7 +76,7 @@ export class Icon extends React.PureComponent<IconProps> {
const iconProps: Partial<IconProps> = { const iconProps: Partial<IconProps> = {
className: cssNames("Icon", className, className: cssNames("Icon", className,
{ svg, material, interactive: isInteractive, disabled, sticker, active, focusable }, { svg, material, interactive: isInteractive, disabled, sticker, active, focusable },
!size ? { small, big } : {} !size ? { smallest, small, big } : {}
), ),
onClick: isInteractive ? this.onClick : undefined, onClick: isInteractive ? this.onClick : undefined,
onKeyDown: isInteractive ? this.onKeyDown : undefined, onKeyDown: isInteractive ? this.onKeyDown : undefined,

View File

@ -1,5 +1,6 @@
import "../common/system-ca" import "../common/system-ca"
import React from "react"; import React from "react";
import { ipcRenderer } from "electron";
import { Route, Router, Switch } from "react-router"; import { Route, Router, Switch } from "react-router";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { userStore } from "../common/user-store"; import { userStore } from "../common/user-store";
@ -17,6 +18,12 @@ import { extensionLoader } from "../extensions/extension-loader";
export class LensApp extends React.Component { export class LensApp extends React.Component {
static async init() { static async init() {
extensionLoader.loadOnClusterManagerRenderer(); extensionLoader.loadOnClusterManagerRenderer();
window.addEventListener("offline", () => {
ipcRenderer.send("network:offline")
})
window.addEventListener("online", () => {
ipcRenderer.send("network:online")
})
} }
render() { render() {