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

Add a few missing folders to be linted.

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
This commit is contained in:
Panu Horsmalahti 2020-11-19 17:05:26 +02:00 committed by Alex Andreev
parent ca67caea60
commit c7b24c2922
29 changed files with 399 additions and 397 deletions

View File

@ -5,7 +5,7 @@ module.exports = {
getVersion: jest.fn().mockReturnValue("3.0.0"), getVersion: jest.fn().mockReturnValue("3.0.0"),
getLocale: jest.fn().mockRejectedValue("en"), getLocale: jest.fn().mockRejectedValue("en"),
getPath: jest.fn((name: string) => { getPath: jest.fn((name: string) => {
return "tmp" return "tmp";
}), }),
}, },
remote: { remote: {

View File

@ -1,9 +1,9 @@
// Generate tray icons from SVG to PNG + different sizes and colors (B&W) // Generate tray icons from SVG to PNG + different sizes and colors (B&W)
// Command: `yarn build:tray-icons` // Command: `yarn build:tray-icons`
import path from "path" import path from "path";
import sharp from "sharp"; import sharp from "sharp";
import jsdom from "jsdom" import jsdom from "jsdom";
import fs from "fs-extra" import fs from "fs-extra";
export async function generateTrayIcon( export async function generateTrayIcon(
{ {
@ -14,15 +14,15 @@ export async function generateTrayIcon(
pixelSize = 32, pixelSize = 32,
shouldUseDarkColors = false, // managed by electron.nativeTheme.shouldUseDarkColors shouldUseDarkColors = false, // managed by electron.nativeTheme.shouldUseDarkColors
} = {}) { } = {}) {
outputFilename += shouldUseDarkColors ? "_dark" : "" outputFilename += shouldUseDarkColors ? "_dark" : "";
dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : "" dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : "";
const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`) const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`);
try { try {
// Modify .SVG colors // Modify .SVG colors
const trayIconColor = shouldUseDarkColors ? "white" : "black"; const trayIconColor = shouldUseDarkColors ? "white" : "black";
const svgDom = await jsdom.JSDOM.fromFile(svgIconPath); const svgDom = await jsdom.JSDOM.fromFile(svgIconPath);
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0]; const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>` svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>`;
const svgIconBuffer = Buffer.from(svgRoot.outerHTML); const svgIconBuffer = Buffer.from(svgRoot.outerHTML);
// Resize and convert to .PNG // Resize and convert to .PNG

View File

@ -1,3 +1,3 @@
import { helmCli } from "../src/main/helm/helm-cli" import { helmCli } from "../src/main/helm/helm-cli";
helmCli.ensureBinary() helmCli.ensureBinary();

View File

@ -1,10 +1,10 @@
import packageInfo from "../package.json" import packageInfo from "../package.json";
import fs from "fs" import fs from "fs";
import request from "request" import request from "request";
import md5File from "md5-file" import md5File from "md5-file";
import requestPromise from "request-promise-native" import requestPromise from "request-promise-native";
import { ensureDir, pathExists } from "fs-extra" import { ensureDir, pathExists } from "fs-extra";
import path from "path" import path from "path";
class KubectlDownloader { class KubectlDownloader {
public kubectlVersion: string public kubectlVersion: string
@ -14,7 +14,7 @@ class KubectlDownloader {
constructor(clusterVersion: string, platform: string, arch: string, target: string) { constructor(clusterVersion: string, platform: string, arch: string, target: string) {
this.kubectlVersion = clusterVersion; this.kubectlVersion = clusterVersion;
const binaryName = platform === "windows" ? "kubectl.exe" : "kubectl" const binaryName = platform === "windows" ? "kubectl.exe" : "kubectl";
this.url = `https://storage.googleapis.com/kubernetes-release/release/v${this.kubectlVersion}/bin/${platform}/${arch}/${binaryName}`; this.url = `https://storage.googleapis.com/kubernetes-release/release/v${this.kubectlVersion}/bin/${platform}/${arch}/${binaryName}`;
this.dirname = path.dirname(target); this.dirname = path.dirname(target);
this.path = target; this.path = target;
@ -25,83 +25,85 @@ class KubectlDownloader {
method: "HEAD", method: "HEAD",
uri: this.url, uri: this.url,
resolveWithFullResponse: true resolveWithFullResponse: true
}).catch((error) => { console.log(error) }) }).catch((error) => { console.log(error); });
if (response.headers["etag"]) { if (response.headers["etag"]) {
return response.headers["etag"].replace(/"/g, "") return response.headers["etag"].replace(/"/g, "");
} }
return "" return "";
} }
public async checkBinary() { public async checkBinary() {
const exists = await pathExists(this.path) const exists = await pathExists(this.path);
if (exists) { if (exists) {
const hash = md5File.sync(this.path) const hash = md5File.sync(this.path);
const etag = await this.urlEtag() const etag = await this.urlEtag();
if(hash == etag) { if(hash == etag) {
console.log("Kubectl md5sum matches the remote etag") console.log("Kubectl md5sum matches the remote etag");
return true return true;
} }
console.log("Kubectl md5sum " + hash + " does not match the remote etag " + etag + ", unlinking and downloading again") console.log("Kubectl md5sum " + hash + " does not match the remote etag " + etag + ", unlinking and downloading again");
await fs.promises.unlink(this.path) await fs.promises.unlink(this.path);
} }
return false return false;
} }
public async downloadKubectl() { public async downloadKubectl() {
const exists = await this.checkBinary(); const exists = await this.checkBinary();
if(exists) { if(exists) {
console.log("Already exists and is valid") console.log("Already exists and is valid");
return return;
} }
await ensureDir(path.dirname(this.path), 0o755) await ensureDir(path.dirname(this.path), 0o755);
const file = fs.createWriteStream(this.path) const file = fs.createWriteStream(this.path);
console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`) console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
const requestOpts: request.UriOptions & request.CoreOptions = { const requestOpts: request.UriOptions & request.CoreOptions = {
uri: this.url, uri: this.url,
gzip: true gzip: true
} };
const stream = request(requestOpts) const stream = request(requestOpts);
stream.on("complete", () => { stream.on("complete", () => {
console.log("kubectl binary download finished") console.log("kubectl binary download finished");
file.end(() => {}) // eslint-disable-next-line @typescript-eslint/no-empty-function
}) file.end(() => {});
});
stream.on("error", (error) => { stream.on("error", (error) => {
console.log(error) console.log(error);
fs.unlink(this.path, () => {}) // eslint-disable-next-line @typescript-eslint/no-empty-function
throw(error) fs.unlink(this.path, () => {});
}) throw(error);
});
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
file.on("close", () => { file.on("close", () => {
console.log("kubectl binary download closed") console.log("kubectl binary download closed");
fs.chmod(this.path, 0o755, (err) => { fs.chmod(this.path, 0o755, (err) => {
if (err) reject(err); if (err) reject(err);
}) });
resolve() resolve();
}) });
stream.pipe(file) stream.pipe(file);
}) });
} }
} }
const downloadVersion = packageInfo.config.bundledKubectlVersion; const downloadVersion = packageInfo.config.bundledKubectlVersion;
const baseDir = path.join(process.env.INIT_CWD, 'binaries', 'client') const baseDir = path.join(process.env.INIT_CWD, 'binaries', 'client');
const downloads = [ const downloads = [
{ platform: 'linux', arch: 'amd64', target: path.join(baseDir, 'linux', 'x64', 'kubectl') }, { platform: 'linux', arch: 'amd64', target: path.join(baseDir, 'linux', 'x64', 'kubectl') },
{ platform: 'darwin', arch: 'amd64', target: path.join(baseDir, 'darwin', 'x64', 'kubectl') }, { platform: 'darwin', arch: 'amd64', target: path.join(baseDir, 'darwin', 'x64', 'kubectl') },
{ platform: 'windows', arch: 'amd64', target: path.join(baseDir, 'windows', 'x64', 'kubectl.exe') }, { platform: 'windows', arch: 'amd64', target: path.join(baseDir, 'windows', 'x64', 'kubectl.exe') },
{ platform: 'windows', arch: '386', target: path.join(baseDir, 'windows', 'ia32', 'kubectl.exe') } { platform: 'windows', arch: '386', target: path.join(baseDir, 'windows', 'ia32', 'kubectl.exe') }
] ];
downloads.forEach((dlOpts) => { downloads.forEach((dlOpts) => {
console.log(dlOpts) console.log(dlOpts);
const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target); const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target);
console.log("Downloading: " + JSON.stringify(dlOpts)); console.log("Downloading: " + JSON.stringify(dlOpts));
downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete"))) downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete")));
}) });

View File

@ -1,4 +1,4 @@
const { notarize } = require('electron-notarize') const { notarize } = require('electron-notarize');
exports.default = async function notarizing(context) { exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context; const { electronPlatformName, appOutDir } = context;

View File

@ -1,9 +1,9 @@
import * as fs from "fs" import * as fs from "fs";
import * as path from "path" import * as path from "path";
import packageInfo from "../src/extensions/npm/extensions/package.json" import packageInfo from "../src/extensions/npm/extensions/package.json";
import appInfo from "../package.json" import appInfo from "../package.json";
const packagePath = path.join(__dirname, "../src/extensions/npm/extensions/package.json") const packagePath = path.join(__dirname, "../src/extensions/npm/extensions/package.json");
packageInfo.version = appInfo.version packageInfo.version = appInfo.version;
fs.writeFileSync(packagePath, JSON.stringify(packageInfo, null, 2) + "\n") fs.writeFileSync(packagePath, JSON.stringify(packageInfo, null, 2) + "\n");

View File

@ -1,10 +1,10 @@
import { LensRendererExtension, Component } from "@k8slens/extensions"; import { LensRendererExtension, Component } from "@k8slens/extensions";
import { CoffeeDoodle } from "react-open-doodles"; import { CoffeeDoodle } from "react-open-doodles";
import path from "path"; import path from "path";
import React from "react" import React from "react";
export function ExampleIcon(props: Component.IconProps) { export function ExampleIcon(props: Component.IconProps) {
return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/> return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/>;
} }
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> { export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
@ -16,7 +16,7 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens
render() { render() {
const doodleStyle = { const doodleStyle = {
width: "200px" width: "200px"
} };
return ( return (
<div className="flex column gaps align-flex-start"> <div className="flex column gaps align-flex-start">
<div style={doodleStyle}><CoffeeDoodle accent="#3d90ce" /></div> <div style={doodleStyle}><CoffeeDoodle accent="#3d90ce" /></div>
@ -24,6 +24,6 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens
<p>File: <i>{__filename}</i></p> <p>File: <i>{__filename}</i></p>
<Component.Button accent label="Deactivate" onClick={this.deactivate}/> <Component.Button accent label="Deactivate" onClick={this.deactivate}/>
</div> </div>
) );
} }
} }

View File

@ -1,6 +1,6 @@
import { LensRendererExtension } from "@k8slens/extensions"; import { LensRendererExtension } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page" import { ExampleIcon, ExamplePage } from "./page";
import React from "react" import React from "react";
export default class ExampleExtension extends LensRendererExtension { export default class ExampleExtension extends LensRendererExtension {
clusterPages = [ clusterPages = [

View File

@ -1,5 +1,5 @@
import { LensRendererExtension, K8sApi } from "@k8slens/extensions"; import { LensRendererExtension, K8sApi } from "@k8slens/extensions";
import { resolveStatus, resolveStatusForCronJobs, resolveStatusForPods } from "./src/resolver" import { resolveStatus, resolveStatusForCronJobs, resolveStatusForPods } from "./src/resolver";
export default class EventResourceStatusRendererExtension extends LensRendererExtension { export default class EventResourceStatusRendererExtension extends LensRendererExtension {
kubeObjectStatusTexts = [ kubeObjectStatusTexts = [

View File

@ -1,9 +1,9 @@
import { K8sApi } from "@k8slens/extensions"; import { K8sApi } from "@k8slens/extensions";
export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatus { export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatus {
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi) const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
const events = (eventStore as K8sApi.EventStore).getEventsByObject(object); const events = (eventStore as K8sApi.EventStore).getEventsByObject(object);
let warnings = events.filter(evt => evt.isWarning()); const warnings = events.filter(evt => evt.isWarning());
if (!events.length || !warnings.length) { if (!events.length || !warnings.length) {
return null; return null;
} }
@ -12,16 +12,16 @@ export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatu
level: K8sApi.KubeObjectStatusLevel.WARNING, level: K8sApi.KubeObjectStatusLevel.WARNING,
text: `${event.message}`, text: `${event.message}`,
timestamp: event.metadata.creationTimestamp timestamp: event.metadata.creationTimestamp
} };
} }
export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus { export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus {
if (!pod.hasIssues()) { if (!pod.hasIssues()) {
return null return null;
} }
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi) const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
const events = (eventStore as K8sApi.EventStore).getEventsByObject(pod); const events = (eventStore as K8sApi.EventStore).getEventsByObject(pod);
let warnings = events.filter(evt => evt.isWarning()); const warnings = events.filter(evt => evt.isWarning());
if (!events.length || !warnings.length) { if (!events.length || !warnings.length) {
return null; return null;
} }
@ -30,13 +30,13 @@ export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus {
level: K8sApi.KubeObjectStatusLevel.WARNING, level: K8sApi.KubeObjectStatusLevel.WARNING,
text: `${event.message}`, text: `${event.message}`,
timestamp: event.metadata.creationTimestamp timestamp: event.metadata.creationTimestamp
} };
} }
export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeObjectStatus { export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeObjectStatus {
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi) const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
let events = (eventStore as K8sApi.EventStore).getEventsByObject(cronJob); let events = (eventStore as K8sApi.EventStore).getEventsByObject(cronJob);
let warnings = events.filter(evt => evt.isWarning()); const warnings = events.filter(evt => evt.isWarning());
if (cronJob.isNeverRun()) { if (cronJob.isNeverRun()) {
events = events.filter(event => event.reason != "FailedNeedsStart"); events = events.filter(event => event.reason != "FailedNeedsStart");
} }
@ -48,5 +48,5 @@ export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeOb
level: K8sApi.KubeObjectStatusLevel.WARNING, level: K8sApi.KubeObjectStatusLevel.WARNING,
text: `${event.message}`, text: `${event.message}`,
timestamp: event.metadata.creationTimestamp timestamp: event.metadata.creationTimestamp
} };
} }

View File

@ -6,7 +6,7 @@ export default class LicenseLensMainExtension extends LensMainExtension {
parentId: "help", parentId: "help",
label: "License", label: "License",
async click() { async click() {
Util.openExternal("https://k8slens.dev/licenses/eula.md") Util.openExternal("https://k8slens.dev/licenses/eula.md");
} }
} }
] ]

View File

@ -1,4 +1,4 @@
import path from "path" import path from "path";
const outputPath = path.resolve(__dirname, 'dist'); const outputPath = path.resolve(__dirname, 'dist');

View File

@ -1,6 +1,6 @@
import { LensRendererExtension } from "@k8slens/extensions" import { LensRendererExtension } from "@k8slens/extensions";
import { MetricsFeature } from "./src/metrics-feature" import { MetricsFeature } from "./src/metrics-feature";
import React from "react" import React from "react";
export default class ClusterMetricsFeatureExtension extends LensRendererExtension { export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
clusterFeatures = [ clusterFeatures = [
@ -14,7 +14,7 @@ export default class ClusterMetricsFeatureExtension extends LensRendererExtensio
Install this only if you don't have existing Prometheus stack installed. Install this only if you don't have existing Prometheus stack installed.
You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>. You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>.
</span> </span>
) );
} }
}, },
feature: new MetricsFeature() feature: new MetricsFeature()

View File

@ -1,6 +1,6 @@
import { ClusterFeature, Store, K8sApi } from "@k8slens/extensions" import { ClusterFeature, Store, K8sApi } from "@k8slens/extensions";
import semver from "semver" import semver from "semver";
import * as path from "path" import * as path from "path";
export interface MetricsConfiguration { export interface MetricsConfiguration {
// Placeholder for Metrics config structure // Placeholder for Metrics config structure
@ -51,46 +51,46 @@ export class MetricsFeature extends ClusterFeature.Feature {
async install(cluster: Store.Cluster): Promise<void> { async install(cluster: Store.Cluster): Promise<void> {
// Check if there are storageclasses // Check if there are storageclasses
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass) const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
const scs = await storageClassApi.list() const scs = await storageClassApi.list();
this.config.persistence.enabled = scs.some(sc => ( this.config.persistence.enabled = scs.some(sc => (
sc.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'] === 'true' || sc.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'] === 'true' ||
sc.metadata?.annotations?.['storageclass.beta.kubernetes.io/is-default-class'] === 'true' sc.metadata?.annotations?.['storageclass.beta.kubernetes.io/is-default-class'] === 'true'
)); ));
super.applyResources(cluster, super.renderTemplates(path.join(__dirname, "../resources/"))) super.applyResources(cluster, super.renderTemplates(path.join(__dirname, "../resources/")));
} }
async upgrade(cluster: Store.Cluster): Promise<void> { async upgrade(cluster: Store.Cluster): Promise<void> {
return this.install(cluster) return this.install(cluster);
} }
async updateStatus(cluster: Store.Cluster): Promise<ClusterFeature.FeatureStatus> { async updateStatus(cluster: Store.Cluster): Promise<ClusterFeature.FeatureStatus> {
try { try {
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet) const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet);
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"}) const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
if (prometheus?.kind) { if (prometheus?.kind) {
this.status.installed = true; this.status.installed = true;
this.status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1]; this.status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];
this.status.canUpgrade = semver.lt(this.status.currentVersion, this.latestVersion, true); this.status.canUpgrade = semver.lt(this.status.currentVersion, this.latestVersion, true);
} else { } else {
this.status.installed = false this.status.installed = false;
} }
} catch(e) { } catch(e) {
if (e?.error?.code === 404) { if (e?.error?.code === 404) {
this.status.installed = false this.status.installed = false;
} }
} }
return this.status return this.status;
} }
async uninstall(cluster: Store.Cluster): Promise<void> { async uninstall(cluster: Store.Cluster): Promise<void> {
const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace) const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace);
const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding) const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding);
const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole) const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole);
await namespaceApi.delete({name: "lens-metrics"}) await namespaceApi.delete({name: "lens-metrics"});
await clusterRoleBindingApi.delete({name: "lens-prometheus"}) await clusterRoleBindingApi.delete({name: "lens-prometheus"});
await clusterRoleApi.delete({name: "lens-prometheus"}) } await clusterRoleApi.delete({name: "lens-prometheus"}); }
} }

View File

@ -1,6 +1,6 @@
import { LensRendererExtension } from "@k8slens/extensions"; import { LensRendererExtension } from "@k8slens/extensions";
import React from "react" import React from "react";
import { NodeMenu, NodeMenuProps } from "./src/node-menu" import { NodeMenu, NodeMenuProps } from "./src/node-menu";
export default class NodeMenuRendererExtension extends LensRendererExtension { export default class NodeMenuRendererExtension extends LensRendererExtension {
kubeObjectMenuItems = [ kubeObjectMenuItems = [

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { Component, K8sApi, Navigation} from "@k8slens/extensions" import { Component, K8sApi, Navigation} from "@k8slens/extensions";
export interface NodeMenuProps extends Component.KubeObjectMenuProps<K8sApi.Node> { export interface NodeMenuProps extends Component.KubeObjectMenuProps<K8sApi.Node> {
} }
@ -15,7 +15,7 @@ export function NodeMenu(props: NodeMenuProps) {
newTab: true, newTab: true,
}); });
Navigation.hideDetails(); Navigation.hideDetails();
} };
const shell = () => { const shell = () => {
Component.createTerminalTab({ Component.createTerminalTab({
@ -23,15 +23,15 @@ export function NodeMenu(props: NodeMenuProps) {
node: nodeName, node: nodeName,
}); });
Navigation.hideDetails(); Navigation.hideDetails();
} };
const cordon = () => { const cordon = () => {
sendToTerminal(`kubectl cordon ${nodeName}`); sendToTerminal(`kubectl cordon ${nodeName}`);
} };
const unCordon = () => { const unCordon = () => {
sendToTerminal(`kubectl uncordon ${nodeName}`) sendToTerminal(`kubectl uncordon ${nodeName}`);
} };
const drain = () => { const drain = () => {
const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`; const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`;
@ -43,8 +43,8 @@ export function NodeMenu(props: NodeMenuProps) {
Are you sure you want to drain <b>{nodeName}</b>? Are you sure you want to drain <b>{nodeName}</b>?
</p> </p>
), ),
}) });
} };
return ( return (
<> <>

View File

@ -1,7 +1,7 @@
import { LensRendererExtension } from "@k8slens/extensions"; import { LensRendererExtension } from "@k8slens/extensions";
import { PodShellMenu, PodShellMenuProps } from "./src/shell-menu" import { PodShellMenu, PodShellMenuProps } from "./src/shell-menu";
import { PodLogsMenu, PodLogsMenuProps } from "./src/logs-menu" import { PodLogsMenu, PodLogsMenuProps } from "./src/logs-menu";
import React from "react" import React from "react";
export default class PodMenuRendererExtension extends LensRendererExtension { export default class PodMenuRendererExtension extends LensRendererExtension {
kubeObjectMenuItems = [ kubeObjectMenuItems = [

View File

@ -19,7 +19,7 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
} }
render() { render() {
const { object: pod, toolbar } = this.props const { object: pod, toolbar } = this.props;
const containers = pod.getAllContainers(); const containers = pod.getAllContainers();
const statuses = pod.getContainerStatuses(); const statuses = pod.getContainerStatuses();
if (!containers.length) return null; if (!containers.length) return null;
@ -33,25 +33,25 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
<Component.SubMenu> <Component.SubMenu>
{ {
containers.map(container => { containers.map(container => {
const { name } = container const { name } = container;
const status = statuses.find(status => status.name === name); const status = statuses.find(status => status.name === name);
const brick = status ? ( const brick = status ? (
<Component.StatusBrick <Component.StatusBrick
className={Util.cssNames(Object.keys(status.state)[0], { ready: status.ready })} className={Util.cssNames(Object.keys(status.state)[0], { ready: status.ready })}
/> />
) : null ) : null;
return ( return (
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center"> <Component.MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center">
{brick} {brick}
{name} {name}
</Component.MenuItem> </Component.MenuItem>
) );
}) })
} }
</Component.SubMenu> </Component.SubMenu>
</> </>
)} )}
</Component.MenuItem> </Component.MenuItem>
) );
} }
} }

View File

@ -9,16 +9,16 @@ export interface PodShellMenuProps extends Component.KubeObjectMenuProps<K8sApi.
export class PodShellMenu extends React.Component<PodShellMenuProps> { export class PodShellMenu extends React.Component<PodShellMenuProps> {
async execShell(container?: string) { async execShell(container?: string) {
Navigation.hideDetails(); Navigation.hideDetails();
const { object: pod } = this.props const { object: pod } = this.props;
const containerParam = container ? `-c ${container}` : "" const containerParam = container ? `-c ${container}` : "";
let command = `kubectl exec -i -t -n ${pod.getNs()} ${pod.getName()} ${containerParam} "--"` let command = `kubectl exec -i -t -n ${pod.getNs()} ${pod.getName()} ${containerParam} "--"`;
if (window.navigator.platform !== "Win32") { if (window.navigator.platform !== "Win32") {
command = `exec ${command}` command = `exec ${command}`;
} }
if (pod.getSelectedNodeOs() === "windows") { if (pod.getSelectedNodeOs() === "windows") {
command = `${command} powershell` command = `${command} powershell`;
} else { } else {
command = `${command} sh -c "clear; (bash || ash || sh)"` command = `${command} sh -c "clear; (bash || ash || sh)"`;
} }
const shell = Component.createTerminalTab({ const shell = Component.createTerminalTab({
@ -32,7 +32,7 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
} }
render() { render() {
const { object, toolbar } = this.props const { object, toolbar } = this.props;
const containers = object.getRunningContainers(); const containers = object.getRunningContainers();
if (!containers.length) return null; if (!containers.length) return null;
return ( return (
@ -51,13 +51,13 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
<Component.StatusBrick/> <Component.StatusBrick/>
{name} {name}
</Component.MenuItem> </Component.MenuItem>
) );
}) })
} }
</Component.SubMenu> </Component.SubMenu>
</> </>
)} )}
</Component.MenuItem> </Component.MenuItem>
) );
} }
} }

View File

@ -1,8 +1,8 @@
// TODO: support localization / figure out how to extract / consume i18n strings // TODO: support localization / figure out how to extract / consume i18n strings
import "./support.scss" import "./support.scss";
import React from "react" import React from "react";
import { observer } from "mobx-react" import { observer } from "mobx-react";
import { App, Component } from "@k8slens/extensions"; import { App, Component } from "@k8slens/extensions";
@observer @observer

View File

@ -1,4 +1,4 @@
import path from "path" import path from "path";
const outputPath = path.resolve(__dirname, 'dist'); const outputPath = path.resolve(__dirname, 'dist');

View File

@ -1,18 +1,18 @@
import { LensMainExtension } from "@k8slens/extensions"; import { LensMainExtension } from "@k8slens/extensions";
import { telemetryPreferencesStore } from "./src/telemetry-preferences-store" import { telemetryPreferencesStore } from "./src/telemetry-preferences-store";
import { tracker } from "./src/tracker"; import { tracker } from "./src/tracker";
export default class TelemetryMainExtension extends LensMainExtension { 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() tracker.reportPeriodically();
await telemetryPreferencesStore.loadExtension(this) await telemetryPreferencesStore.loadExtension(this);
} }
onDeactivate() { onDeactivate() {
tracker.stop() tracker.stop();
console.log("telemetry main extension deactivated") console.log("telemetry main extension deactivated");
} }
} }

View File

@ -1,8 +1,8 @@
import { LensRendererExtension } from "@k8slens/extensions"; import { LensRendererExtension } from "@k8slens/extensions";
import { telemetryPreferencesStore } from "./src/telemetry-preferences-store" import { telemetryPreferencesStore } from "./src/telemetry-preferences-store";
import { TelemetryPreferenceHint, TelemetryPreferenceInput } from "./src/telemetry-preference" import { TelemetryPreferenceHint, TelemetryPreferenceInput } from "./src/telemetry-preference";
import { tracker } from "./src/tracker" import { tracker } from "./src/tracker";
import React from "react" import React from "react";
export default class TelemetryRendererExtension extends LensRendererExtension { export default class TelemetryRendererExtension extends LensRendererExtension {
appPreferences = [ appPreferences = [
@ -16,8 +16,8 @@ export default class TelemetryRendererExtension extends LensRendererExtension {
]; ];
async onActivate() { async onActivate() {
console.log("telemetry extension activated") console.log("telemetry extension activated");
tracker.start() tracker.start();
await telemetryPreferencesStore.loadExtension(this) await telemetryPreferencesStore.loadExtension(this);
} }
} }

View File

@ -1,19 +1,19 @@
import { Component } from "@k8slens/extensions" import { Component } from "@k8slens/extensions";
import React from "react" import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TelemetryPreferencesStore } from "./telemetry-preferences-store" import { TelemetryPreferencesStore } from "./telemetry-preferences-store";
@observer @observer
export class TelemetryPreferenceInput extends React.Component<{telemetry: TelemetryPreferencesStore}, {}> { export class TelemetryPreferenceInput extends React.Component<{telemetry: TelemetryPreferencesStore}, {}> {
render() { render() {
const { telemetry } = this.props const { telemetry } = this.props;
return ( return (
<Component.Checkbox <Component.Checkbox
label="Allow telemetry & usage tracking" label="Allow telemetry & usage tracking"
value={telemetry.enabled} value={telemetry.enabled}
onChange={v => { telemetry.enabled = v; }} onChange={v => { telemetry.enabled = v; }}
/> />
) );
} }
} }
@ -21,6 +21,6 @@ export class TelemetryPreferenceHint extends React.Component {
render() { render() {
return ( return (
<span>Telemetry & usage data is collected to continuously improve the Lens experience.</span> <span>Telemetry & usage data is collected to continuously improve the Lens experience.</span>
) );
} }
} }

View File

@ -1,5 +1,5 @@
import { Store } from "@k8slens/extensions"; import { Store } from "@k8slens/extensions";
import { toJS } from "mobx" import { toJS } from "mobx";
export type TelemetryPreferencesModel = { export type TelemetryPreferencesModel = {
enabled: boolean; enabled: boolean;
@ -14,11 +14,11 @@ export class TelemetryPreferencesStore extends Store.ExtensionStore<TelemetryPre
defaults: { defaults: {
enabled: true enabled: true
} }
}) });
} }
protected fromStore({ enabled }: TelemetryPreferencesModel): void { protected fromStore({ enabled }: TelemetryPreferencesModel): void {
this.enabled = enabled this.enabled = enabled;
} }
toJSON(): TelemetryPreferencesModel { toJSON(): TelemetryPreferencesModel {
@ -26,8 +26,8 @@ export class TelemetryPreferencesStore extends Store.ExtensionStore<TelemetryPre
enabled: this.enabled enabled: this.enabled
}, { }, {
recurseEverything: true recurseEverything: true
}) });
} }
} }
export const telemetryPreferencesStore = TelemetryPreferencesStore.getInstance<TelemetryPreferencesStore>() export const telemetryPreferencesStore = TelemetryPreferencesStore.getInstance<TelemetryPreferencesStore>();

View File

@ -1,8 +1,8 @@
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 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"
@ -23,69 +23,69 @@ export class Tracker extends Util.Singleton {
private constructor() { private constructor() {
super(); super();
this.anonymousId = machineIdSync() this.anonymousId = machineIdSync();
this.os = this.resolveOS() this.os = this.resolveOS();
this.userAgent = `Lens ${App.version} (${this.os})` this.userAgent = `Lens ${App.version} (${this.os})`;
try { try {
this.visitor = ua(Tracker.GA_ID, this.anonymousId, { 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.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", this.userAgent) this.visitor.set("ua", this.userAgent);
} }
start() { start() {
if (this.started === true) { return } if (this.started === true) { return; }
this.started = true this.started = true;
const handler = (ev: EventBus.AppEvent) => { const handler = (ev: EventBus.AppEvent) => {
this.event(ev.name, ev.action, ev.params) this.event(ev.name, ev.action, ev.params);
} };
this.eventHandlers.push(handler) this.eventHandlers.push(handler);
EventBus.appEventBus.addListener(handler) EventBus.appEventBus.addListener(handler);
} }
reportPeriodically() { reportPeriodically() {
this.reportInterval = setInterval(() => { this.reportInterval = setInterval(() => {
this.reportData() this.reportData();
}, 60 * 60 * 1000) // report every 1h }, 60 * 60 * 1000); // report every 1h
} }
stop() { stop() {
if (!this.started) { return } if (!this.started) { return; }
this.started = false this.started = false;
for (const handler of this.eventHandlers) { for (const handler of this.eventHandlers) {
EventBus.appEventBus.removeListener(handler) EventBus.appEventBus.removeListener(handler);
} }
if (this.reportInterval) { if (this.reportInterval) {
clearInterval(this.reportInterval) clearInterval(this.reportInterval);
} }
} }
protected async isTelemetryAllowed(): Promise<boolean> { protected async isTelemetryAllowed(): Promise<boolean> {
return telemetryPreferencesStore.enabled return telemetryPreferencesStore.enabled;
} }
protected reportData() { protected reportData() {
const clustersList = Store.clusterStore.enabledClustersList const clustersList = Store.clusterStore.enabledClustersList;
this.event("generic-data", "report", { this.event("generic-data", "report", {
appVersion: App.version, appVersion: App.version,
os: this.os, os: this.os,
clustersCount: clustersList.length, clustersCount: clustersList.length,
workspacesCount: Store.workspaceStore.enabledWorkspacesList.length workspacesCount: Store.workspaceStore.enabledWorkspacesList.length
}) });
clustersList.forEach((cluster) => { clustersList.forEach((cluster) => {
if (!cluster?.metadata.lastSeen) { return } if (!cluster?.metadata.lastSeen) { return; }
this.reportClusterData(cluster) this.reportClusterData(cluster);
}) });
} }
protected reportClusterData(cluster: Store.ClusterModel) { protected reportClusterData(cluster: Store.ClusterModel) {
@ -96,26 +96,26 @@ export class Tracker extends Util.Singleton {
distribution: cluster.metadata.distribution, distribution: cluster.metadata.distribution,
nodesCount: cluster.metadata.nodes, nodesCount: cluster.metadata.nodes,
lastSeen: cluster.metadata.lastSeen lastSeen: cluster.metadata.lastSeen
}) });
} }
protected resolveOS() { protected resolveOS() {
let os = "" let os = "";
if (App.isMac) { if (App.isMac) {
os = "MacOS" os = "MacOS";
} else if(App.isWindows) { } else if(App.isWindows) {
os = "Windows" os = "Windows";
} else if (App.isLinux) { } else if (App.isLinux) {
os = "Linux" os = "Linux";
if (App.isSnap) { if (App.isSnap) {
os += "; Snap" os += "; Snap";
} else { } else {
os += "; AppImage" os += "; AppImage";
} }
} else { } else {
os = "Unknown" os = "Unknown";
} }
return os return os;
} }
protected async event(eventCategory: string, eventAction: string, otherParams = {}) { protected async event(eventCategory: string, eventAction: string, otherParams = {}) {
@ -128,7 +128,7 @@ export class Tracker extends Util.Singleton {
ec: eventCategory, ec: eventCategory,
ea: eventAction, ea: eventAction,
...otherParams, ...otherParams,
}).send() }).send();
this.analytics.track({ this.analytics.track({
anonymousId: this.anonymousId, anonymousId: this.anonymousId,
@ -141,9 +141,9 @@ export class Tracker extends Util.Singleton {
...otherParams, ...otherParams,
}, },
}) });
} catch (err) { } catch (err) {
console.error(`Failed to track "${eventCategory}:${eventAction}"`, err) console.error(`Failed to track "${eventCategory}:${eventAction}"`, err);
} }
} }
} }

View File

@ -4,194 +4,194 @@
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
cluster and vice versa. cluster and vice versa.
*/ */
import { Application } from "spectron" import { Application } from "spectron";
import * as util from "../helpers/utils" import * as util from "../helpers/utils";
import { spawnSync } from "child_process" import { spawnSync } from "child_process";
const describeif = (condition: boolean) => condition ? describe : describe.skip const describeif = (condition: boolean) => condition ? describe : describe.skip;
const itif = (condition: boolean) => condition ? it : it.skip const itif = (condition: boolean) => condition ? it : it.skip;
jest.setTimeout(60000) jest.setTimeout(60000);
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) // FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
describe("Lens integration tests", () => { describe("Lens integration tests", () => {
const TEST_NAMESPACE = "integration-tests" const TEST_NAMESPACE = "integration-tests";
const BACKSPACE = "\uE003" const BACKSPACE = "\uE003";
let app: Application let app: Application;
const appStart = async () => { const appStart = async () => {
app = util.setup() app = util.setup();
await app.start() await app.start();
// Wait for splash screen to be closed // Wait for splash screen to be closed
while (await app.client.getWindowCount() > 1); while (await app.client.getWindowCount() > 1);
await app.client.windowByIndex(0) await app.client.windowByIndex(0);
await app.client.waitUntilWindowLoaded() await app.client.waitUntilWindowLoaded();
} };
const clickWhatsNew = async (app: Application) => { const clickWhatsNew = async (app: Application) => {
await app.client.waitUntilTextExists("h1", "What's new?") await app.client.waitUntilTextExists("h1", "What's new?");
await app.client.click("button.primary") await app.client.click("button.primary");
await app.client.waitUntilTextExists("h1", "Welcome") await app.client.waitUntilTextExists("h1", "Welcome");
} };
describe("app start", () => { describe("app start", () => {
beforeAll(appStart, 20000) beforeAll(appStart, 20000);
afterAll(async () => { afterAll(async () => {
if (app && app.isRunning()) { if (app && app.isRunning()) {
return util.tearDown(app) return util.tearDown(app);
} }
}) });
it('shows "whats new"', async () => { it('shows "whats new"', async () => {
await clickWhatsNew(app) await clickWhatsNew(app);
}) });
it('shows "add cluster"', async () => { it('shows "add cluster"', async () => {
await app.electron.ipcRenderer.send('test-menu-item-click', "File", "Add Cluster") await app.electron.ipcRenderer.send('test-menu-item-click', "File", "Add Cluster");
await app.client.waitUntilTextExists("h2", "Add Cluster") await app.client.waitUntilTextExists("h2", "Add Cluster");
}) });
describe("preferences page", () => { describe("preferences page", () => {
it('shows "preferences"', async () => { it('shows "preferences"', async () => {
const appName: string = process.platform === "darwin" ? "Lens" : "File" const appName: string = process.platform === "darwin" ? "Lens" : "File";
await app.electron.ipcRenderer.send('test-menu-item-click', appName, "Preferences") await app.electron.ipcRenderer.send('test-menu-item-click', appName, "Preferences");
await app.client.waitUntilTextExists("h2", "Preferences") await app.client.waitUntilTextExists("h2", "Preferences");
}) });
it('ensures helm repos', async () => { it('ensures helm repos', async () => {
await app.client.waitUntilTextExists("div.repos #message-stable", "stable") // wait for the helm-cli to fetch the stable repo await app.client.waitUntilTextExists("div.repos #message-stable", "stable"); // wait for the helm-cli to fetch the stable repo
await app.client.click("#HelmRepoSelect") // click the repo select to activate the drop-down await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down
await app.client.waitUntilTextExists("div.Select__option", "") // wait for at least one option to appear (any text) await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text)
}) });
}) });
it.skip('quits Lens"', async () => { it.skip('quits Lens"', async () => {
await app.client.keys(['Meta', 'Q']) await app.client.keys(['Meta', 'Q']);
await app.client.keys('Meta') await app.client.keys('Meta');
}) });
}) });
describe("workspaces", () => { describe("workspaces", () => {
beforeAll(appStart, 20000) beforeAll(appStart, 20000);
afterAll(async () => { afterAll(async () => {
if (app && app.isRunning()) { if (app && app.isRunning()) {
return util.tearDown(app) return util.tearDown(app);
} }
}) });
it('switches between workspaces', async () => { it('switches between workspaces', async () => {
await clickWhatsNew(app) await clickWhatsNew(app);
await app.client.click('#current-workspace .Icon') await app.client.click('#current-workspace .Icon');
await app.client.click('a[href="/workspaces"]') await app.client.click('a[href="/workspaces"]');
await app.client.click('.Workspaces button.Button') await app.client.click('.Workspaces button.Button');
await app.client.keys("test-workspace") await app.client.keys("test-workspace");
await app.client.click('.Workspaces .Input.description input') await app.client.click('.Workspaces .Input.description input');
await app.client.keys("test description") await app.client.keys("test description");
await app.client.click('.Workspaces .workspace.editing .Icon') await app.client.click('.Workspaces .workspace.editing .Icon');
await app.client.waitUntilTextExists(".workspace .name a", "test-workspace") await app.client.waitUntilTextExists(".workspace .name a", "test-workspace");
await addMinikubeCluster(app) await addMinikubeCluster(app);
await app.client.waitForExist(`iframe[name="minikube"]`) await app.client.waitForExist(`iframe[name="minikube"]`);
await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active") await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active");
// Go to test-workspace // Go to test-workspace
await app.client.click('#current-workspace .Icon') await app.client.click('#current-workspace .Icon');
await app.client.click('.WorkspaceMenu li[title="test description"]') await app.client.click('.WorkspaceMenu li[title="test description"]');
await addMinikubeCluster(app) await addMinikubeCluster(app);
// Back to default one // Back to default one
await app.client.click('#current-workspace .Icon') await app.client.click('#current-workspace .Icon');
await app.client.click('.WorkspaceMenu > li:first-of-type') await app.client.click('.WorkspaceMenu > li:first-of-type');
await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active") await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active");
}) });
}) });
const minikubeReady = (): boolean => { const minikubeReady = (): boolean => {
// determine if minikube is running // determine if minikube is running
let status = spawnSync("minikube status", { shell: true }) let status = spawnSync("minikube status", { shell: true });
if (status.status !== 0) { if (status.status !== 0) {
console.warn("minikube not running") console.warn("minikube not running");
return false return false;
} }
// Remove TEST_NAMESPACE if it already exists // Remove TEST_NAMESPACE if it already exists
status = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true }) status = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true });
if (status.status === 0) { if (status.status === 0) {
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`) console.warn(`Removing existing ${TEST_NAMESPACE} namespace`);
status = spawnSync(`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, { shell: true }) status = spawnSync(`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, { shell: true });
if (status.status !== 0) { if (status.status !== 0) {
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${status.stderr.toString()}`) console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${status.stderr.toString()}`);
return false return false;
} }
console.log(status.stdout.toString()) console.log(status.stdout.toString());
} }
return true return true;
} };
const ready = minikubeReady() const ready = minikubeReady();
const addMinikubeCluster = async (app: Application) => { const addMinikubeCluster = async (app: Application) => {
await app.client.click("div.add-cluster") await app.client.click("div.add-cluster");
await app.client.waitUntilTextExists("div", "Select kubeconfig file") await app.client.waitUntilTextExists("div", "Select kubeconfig file");
await app.client.click("div.Select__control") // show the context drop-down list await app.client.click("div.Select__control"); // show the context drop-down list
await app.client.waitUntilTextExists("div", "minikube") await app.client.waitUntilTextExists("div", "minikube");
if (!await app.client.$("button.primary").isEnabled()) { if (!await app.client.$("button.primary").isEnabled()) {
await app.client.click("div.minikube") // select minikube context await app.client.click("div.minikube"); // select minikube context
} // else the only context, which must be 'minikube', is automatically selected } // else the only context, which must be 'minikube', is automatically selected
await app.client.click("div.Select__control") // hide the context drop-down list (it might be obscuring the Add cluster(s) button) await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
await app.client.click("button.primary") // add minikube cluster await app.client.click("button.primary"); // add minikube cluster
} };
const waitForMinikubeDashboard = async (app: Application) => { const waitForMinikubeDashboard = async (app: Application) => {
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started") await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
await app.client.waitForExist(`iframe[name="minikube"]`) await app.client.waitForExist(`iframe[name="minikube"]`);
await app.client.frame("minikube") await app.client.frame("minikube");
await app.client.waitUntilTextExists("span.link-text", "Cluster") await app.client.waitUntilTextExists("span.link-text", "Cluster");
} };
describeif(ready)("cluster tests", () => { describeif(ready)("cluster tests", () => {
let clusterAdded = false let clusterAdded = false;
const addCluster = async () => { const addCluster = async () => {
await clickWhatsNew(app) await clickWhatsNew(app);
await addMinikubeCluster(app) await addMinikubeCluster(app);
await waitForMinikubeDashboard(app) await waitForMinikubeDashboard(app);
await app.client.click('a[href="/nodes"]') await app.client.click('a[href="/nodes"]');
await app.client.waitUntilTextExists("div.TableCell", "Ready") await app.client.waitUntilTextExists("div.TableCell", "Ready");
} };
describe("cluster add", () => { describe("cluster add", () => {
beforeAll(appStart, 20000) beforeAll(appStart, 20000);
afterAll(async () => { afterAll(async () => {
if (app && app.isRunning()) { if (app && app.isRunning()) {
return util.tearDown(app) return util.tearDown(app);
} }
}) });
it('allows to add a cluster', async () => { it('allows to add a cluster', async () => {
await addCluster() await addCluster();
clusterAdded = true clusterAdded = true;
}) });
}) });
const appStartAddCluster = async () => { const appStartAddCluster = async () => {
if (clusterAdded) { if (clusterAdded) {
await appStart() await appStart();
await addCluster() await addCluster();
}
} }
};
describe("cluster pages", () => { describe("cluster pages", () => {
beforeAll(appStartAddCluster, 40000) beforeAll(appStartAddCluster, 40000);
afterAll(async () => { afterAll(async () => {
if (app && app.isRunning()) { if (app && app.isRunning()) {
return util.tearDown(app) return util.tearDown(app);
} }
}) });
const tests: { const tests: {
drawer?: string drawer?: string
@ -429,119 +429,119 @@ describe("Lens integration tests", () => {
tests.forEach(({ drawer = "", drawerId = "", pages }) => { tests.forEach(({ drawer = "", drawerId = "", pages }) => {
if (drawer !== "") { if (drawer !== "") {
it(`shows ${drawer} drawer`, async () => { it(`shows ${drawer} drawer`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`) await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`);
await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name) await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name);
}) });
} }
pages.forEach(({ name, href, expectedSelector, expectedText }) => { pages.forEach(({ name, href, expectedSelector, expectedText }) => {
it(`shows ${drawer}->${name} page`, async () => { it(`shows ${drawer}->${name} page`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
await app.client.click(`a[href^="/${href}"]`) await app.client.click(`a[href^="/${href}"]`);
await app.client.waitUntilTextExists(expectedSelector, expectedText) await app.client.waitUntilTextExists(expectedSelector, expectedText);
}) });
}) });
if (drawer !== "") { if (drawer !== "") {
// hide the drawer // hide the drawer
it(`hides ${drawer} drawer`, async () => { it(`hides ${drawer} drawer`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`) await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`);
await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow() await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow();
}) });
} }
}) });
}) });
describe("viewing pod logs", () => { describe("viewing pod logs", () => {
beforeEach(appStartAddCluster, 40000) beforeEach(appStartAddCluster, 40000);
afterEach(async () => { afterEach(async () => {
if (app && app.isRunning()) { if (app && app.isRunning()) {
return util.tearDown(app) return util.tearDown(app);
} }
}) });
it(`shows a logs for a pod`, async () => { it(`shows a logs for a pod`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
// Go to Pods page // Go to Pods page
await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text") await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text");
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods") await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
await app.client.click('a[href^="/pods"]') await app.client.click('a[href^="/pods"]');
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver") await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver");
// Open logs tab in dock // Open logs tab in dock
await app.client.click(".list .TableRow:first-child") await app.client.click(".list .TableRow:first-child");
await app.client.waitForVisible(".Drawer") await app.client.waitForVisible(".Drawer");
await app.client.click(".drawer-title .Menu li:nth-child(2)") await app.client.click(".drawer-title .Menu li:nth-child(2)");
// Check if controls are available // Check if controls are available
await app.client.waitForVisible(".PodLogs .VirtualList") await app.client.waitForVisible(".PodLogs .VirtualList");
await app.client.waitForVisible(".PodLogControls") await app.client.waitForVisible(".PodLogControls");
await app.client.waitForVisible(".PodLogControls .SearchInput") await app.client.waitForVisible(".PodLogControls .SearchInput");
await app.client.waitForVisible(".PodLogControls .SearchInput input") await app.client.waitForVisible(".PodLogControls .SearchInput input");
// Search for semicolon // Search for semicolon
await app.client.keys(":") await app.client.keys(":");
await app.client.waitForVisible(".PodLogs .list span.active") await app.client.waitForVisible(".PodLogs .list span.active");
// Click through controls // Click through controls
await app.client.click(".PodLogControls .timestamps-icon") await app.client.click(".PodLogControls .timestamps-icon");
await app.client.click(".PodLogControls .undo-icon") await app.client.click(".PodLogControls .undo-icon");
}) });
}) });
describe("cluster operations", () => { describe("cluster operations", () => {
beforeEach(appStartAddCluster, 40000) beforeEach(appStartAddCluster, 40000);
afterEach(async () => { afterEach(async () => {
if (app && app.isRunning()) { if (app && app.isRunning()) {
return util.tearDown(app) return util.tearDown(app);
} }
}) });
it('shows default namespace', async () => { it('shows default namespace', async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
await app.client.click('a[href="/namespaces"]') await app.client.click('a[href="/namespaces"]');
await app.client.waitUntilTextExists("div.TableCell", "default") await app.client.waitUntilTextExists("div.TableCell", "default");
await app.client.waitUntilTextExists("div.TableCell", "kube-system") await app.client.waitUntilTextExists("div.TableCell", "kube-system");
}) });
it(`creates ${TEST_NAMESPACE} namespace`, async () => { it(`creates ${TEST_NAMESPACE} namespace`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
await app.client.click('a[href="/namespaces"]') await app.client.click('a[href="/namespaces"]');
await app.client.waitUntilTextExists("div.TableCell", "default") await app.client.waitUntilTextExists("div.TableCell", "default");
await app.client.waitUntilTextExists("div.TableCell", "kube-system") await app.client.waitUntilTextExists("div.TableCell", "kube-system");
await app.client.click("button.add-button") await app.client.click("button.add-button");
await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace") await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace");
await app.client.keys(`${TEST_NAMESPACE}\n`) await app.client.keys(`${TEST_NAMESPACE}\n`);
await app.client.waitForExist(`.name=${TEST_NAMESPACE}`) await app.client.waitForExist(`.name=${TEST_NAMESPACE}`);
}) });
it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => { it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text") await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text");
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods") await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
await app.client.click('a[href^="/pods"]') await app.client.click('a[href^="/pods"]');
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver") await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver");
await app.client.click('.Icon.new-dock-tab') await app.client.click('.Icon.new-dock-tab');
await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource") await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource");
await app.client.click("li.MenuItem.create-resource-tab") await app.client.click("li.MenuItem.create-resource-tab");
await app.client.waitForVisible(".CreateResource div.ace_content") await app.client.waitForVisible(".CreateResource div.ace_content");
// Write pod manifest to editor // Write pod manifest to editor
await app.client.keys("apiVersion: v1\n") await app.client.keys("apiVersion: v1\n");
await app.client.keys("kind: Pod\n") await app.client.keys("kind: Pod\n");
await app.client.keys("metadata:\n") await app.client.keys("metadata:\n");
await app.client.keys(" name: nginx-create-pod-test\n") await app.client.keys(" name: nginx-create-pod-test\n");
await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`) await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`);
await app.client.keys(BACKSPACE + "spec:\n") await app.client.keys(BACKSPACE + "spec:\n");
await app.client.keys(" containers:\n") await app.client.keys(" containers:\n");
await app.client.keys("- name: nginx-create-pod-test\n") await app.client.keys("- name: nginx-create-pod-test\n");
await app.client.keys(" image: nginx:alpine\n") await app.client.keys(" image: nginx:alpine\n");
// Create deployment // Create deployment
await app.client.waitForEnabled("button.Button=Create & Close") await app.client.waitForEnabled("button.Button=Create & Close");
await app.client.click("button.Button=Create & Close") await app.client.click("button.Button=Create & Close");
// Wait until first bits of pod appears on dashboard // Wait until first bits of pod appears on dashboard
await app.client.waitForExist(".name=nginx-create-pod-test") await app.client.waitForExist(".name=nginx-create-pod-test");
// Open pod details // Open pod details
await app.client.click(".name=nginx-create-pod-test") await app.client.click(".name=nginx-create-pod-test");
await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test") await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test");
}) });
}) });
}) });
}) });

View File

@ -4,7 +4,7 @@ const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
"win32": "./dist/win-unpacked/Lens.exe", "win32": "./dist/win-unpacked/Lens.exe",
"linux": "./dist/linux-unpacked/kontena-lens", "linux": "./dist/linux-unpacked/kontena-lens",
"darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens", "darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens",
} };
export function setup(): Application { export function setup(): Application {
return new Application({ return new Application({
@ -16,16 +16,16 @@ export function setup(): Application {
env: { env: {
CICD: "true" CICD: "true"
} }
}) });
} }
export async function tearDown(app: Application) { export async function tearDown(app: Application) {
let mpid: any = app.mainProcess.pid const mpid: any = app.mainProcess.pid;
let pid = await mpid() const pid = await mpid();
await app.stop() await app.stop();
try { try {
process.kill(pid, "SIGKILL"); process.kill(pid, "SIGKILL");
} catch (e) { } catch (e) {
console.error(e) console.error(e);
} }
} }

View File

@ -37,7 +37,7 @@
"download:kubectl": "yarn run ts-node build/download_kubectl.ts", "download:kubectl": "yarn run ts-node build/download_kubectl.ts",
"download:helm": "yarn run ts-node build/download_helm.ts", "download:helm": "yarn run ts-node build/download_helm.ts",
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts", "build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
"lint": "yarn run eslint $@ --ext js,ts,tsx --max-warnings=0 src/", "lint": "yarn run eslint $@ --ext js,ts,tsx --max-warnings=0 src/ integration/ __mocks__/ build/",
"lint:fix": "yarn run lint --fix", "lint:fix": "yarn run lint --fix",
"mkdocs-serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest", "mkdocs-serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest",
"typedocs-extensions-api": "yarn run typedoc --ignoreCompilerErrors --readme docs/extensions/typedoc-readme.md.tpl --name @k8slens/extensions --out docs/extensions/api --mode library --excludePrivate --hideBreadcrumbs --includes src/ src/extensions/extension-api.ts" "typedocs-extensions-api": "yarn run typedoc --ignoreCompilerErrors --readme docs/extensions/typedoc-readme.md.tpl --name @k8slens/extensions --out docs/extensions/api --mode library --excludePrivate --hideBreadcrumbs --includes src/ src/extensions/extension-api.ts"