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

Merge branch 'master' into topbar-history-buttons

This commit is contained in:
Alex Andreev 2021-08-27 07:33:06 +03:00
commit f8f082b676
65 changed files with 1448 additions and 1463 deletions

View File

@ -16,8 +16,8 @@ jobs:
vmImage: windows-2019 vmImage: windows-2019
strategy: strategy:
matrix: matrix:
node_12.x: node_14.x:
node_version: 12.x node_version: 14.x
steps: steps:
- powershell: | - powershell: |
$CI_BUILD_TAG = git describe --tags $CI_BUILD_TAG = git describe --tags
@ -48,12 +48,6 @@ jobs:
GH_TOKEN: $(LENS_IDE_GH_TOKEN) GH_TOKEN: $(LENS_IDE_GH_TOKEN)
displayName: Customize config displayName: Customize config
- script: make node_modules
displayName: Install dependencies
- script: make build-npm
displayName: Generate npm package
- script: make build - script: make build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env: env:
@ -69,8 +63,8 @@ jobs:
vmImage: macOS-10.14 vmImage: macOS-10.14
strategy: strategy:
matrix: matrix:
node_12.x: node_14.x:
node_version: 12.x node_version: 14.x
steps: steps:
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG" - script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
@ -99,15 +93,57 @@ jobs:
GH_TOKEN: $(LENS_IDE_GH_TOKEN) GH_TOKEN: $(LENS_IDE_GH_TOKEN)
displayName: Customize config displayName: Customize config
- script: make node_modules - script: make build
displayName: Install dependencies condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env:
APPLEID: $(APPLEID)
APPLEIDPASS: $(APPLEIDPASS)
CSC_LINK: $(CSC_LINK)
CSC_KEY_PASSWORD: $(CSC_KEY_PASSWORD)
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
BUILD_NUMBER: $(Build.BuildNumber)
displayName: Build
- script: make build-npm - job: macOS-arm64
displayName: Generate npm package pool:
vmImage: macOS-10.14
strategy:
matrix:
node_14.x:
node_version: 14.x
steps:
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
displayName: Set the tag name as an environment variable
- task: NodeTool@0
inputs:
versionSpec: $(node_version)
displayName: Install Node.js
- task: Cache@2
inputs:
key: 'yarn | "$(Agent.OS)" | yarn.lock'
restoreKeys: |
yarn | "$(Agent.OS)"
path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages
- bash: |
set -e
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
rm -rf .lens-ide-overlay/.git
cp -r .lens-ide-overlay/* ./
jq -s '.[0] * .[1]' package.json package.ide.json > package.custom.json && mv package.custom.json package.json
env:
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
displayName: Customize config
- script: make build - script: make build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env: env:
BINARY_ARCH: arm64
APPLEID: $(APPLEID) APPLEID: $(APPLEID)
APPLEIDPASS: $(APPLEIDPASS) APPLEIDPASS: $(APPLEIDPASS)
CSC_LINK: $(CSC_LINK) CSC_LINK: $(CSC_LINK)
@ -122,8 +158,8 @@ jobs:
vmImage: ubuntu-16.04 vmImage: ubuntu-16.04
strategy: strategy:
matrix: matrix:
node_12.x: node_14.x:
node_version: 12.x node_version: 14.x
steps: steps:
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG" - script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
@ -152,23 +188,6 @@ jobs:
GH_TOKEN: $(LENS_IDE_GH_TOKEN) GH_TOKEN: $(LENS_IDE_GH_TOKEN)
displayName: Customize config displayName: Customize config
- script: make node_modules
displayName: Install dependencies
- script: make build-npm
displayName: Generate npm package
- bash: |
sudo chown root:root /
sudo apt-get update && sudo apt-get install -y snapd
sudo snap install snapcraft --classic
echo -n "${SNAP_LOGIN}" | base64 -i -d > snap_login
snapcraft login --with snap_login
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env:
SNAP_LOGIN: $(SNAP_LOGIN)
displayName: Setup snapcraft
- script: make build - script: make build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env: env:

View File

@ -8,7 +8,7 @@ jobs:
if: ${{ contains(github.event.pull_request.labels.*.name, 'area/documentation') }} if: ${{ contains(github.event.pull_request.labels.*.name, 'area/documentation') }}
strategy: strategy:
matrix: matrix:
node-version: [12.x] node-version: [14.x]
steps: steps:
- name: Checkout Release from lens - name: Checkout Release from lens
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [12.x] node-version: [14.x]
steps: steps:
- name: Checkout Release from lens - name: Checkout Release from lens
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [12.x] node-version: [14.x]
steps: steps:
- name: Set up Python 3.7 - name: Set up Python 3.7
uses: actions/setup-python@v2 uses: actions/setup-python@v2
@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [12.x] node-version: [14.x]
needs: verify-docs needs: verify-docs
steps: steps:
- name: Set up Python 3.7 - name: Set up Python 3.7

View File

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [12.x] node-version: [14.x]
steps: steps:
- name: Set up Python 3.7 - name: Set up Python 3.7
uses: actions/setup-python@v2 uses: actions/setup-python@v2
@ -29,7 +29,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
ref: '${{ github.event.inputs.version }}' ref: '${{ github.event.inputs.version }}'
- name: Using Node.js ${{ matrix.node-version }} - name: Using Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
@ -53,15 +53,15 @@ jobs:
rm -fr ./docs/clusters ./docs/contributing ./docs/faq ./docs/getting-started ./docs/helm ./docs/support ./docs/supporting rm -fr ./docs/clusters ./docs/contributing ./docs/faq ./docs/getting-started ./docs/helm ./docs/support ./docs/supporting
sed -i '/Protocol Handlers/d' ./mkdocs.yml sed -i '/Protocol Handlers/d' ./mkdocs.yml
sed -i '/IPC/d' ./mkdocs.yml sed -i '/IPC/d' ./mkdocs.yml
sed -i 's#../../clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/extensions/get-started/your-first-extension.md sed -i 's#../../clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/extensions/get-started/your-first-extension.md
sed -i 's#clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/README.md sed -i 's#clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/README.md
sed -i 's#../../contributing/README.md#https://docs.k8slens.dev/latest/contributing/#g' ./docs/extensions/guides/generator.md sed -i 's#../../contributing/README.md#https://docs.k8slens.dev/latest/contributing/#g' ./docs/extensions/guides/generator.md
- name: git config - name: git config
run: | run: |
git config --local user.email "action@github.com" git config --local user.email "action@github.com"
git config --local user.name "GitHub Action" git config --local user.name "GitHub Action"
- name: mkdocs deploy new release - name: mkdocs deploy new release
run: | run: |
mike deploy --push --force ${{ github.event.inputs.version }} mike deploy --push --force ${{ github.event.inputs.version }}

View File

@ -11,7 +11,7 @@ jobs:
${{ github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'area/extension') }} ${{ github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'area/extension') }}
strategy: strategy:
matrix: matrix:
node-version: [12.x] node-version: [14.x]
steps: steps:
- name: Checkout Release - name: Checkout Release
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [12.x] node-version: [14.x]
steps: steps:
- name: Checkout Release - name: Checkout Release
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-16.04, macos-10.15, windows-2019] os: [ubuntu-16.04, macos-10.15, windows-2019]
node-version: [12.x] node-version: [14.x]
steps: steps:
- name: Checkout Release from lens - name: Checkout Release from lens
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@ -1,3 +1,3 @@
disturl "https://atom.io/download/electron" disturl "https://atom.io/download/electron"
target "9.4.4" target "12.0.15"
runtime "electron" runtime "electron"

View File

@ -3,6 +3,7 @@ CMD_ARGS = $(filter-out $@,$(MAKECMDGOALS))
%: %:
@: @:
BINARY_ARCH ?= x64
NPM_RELEASE_TAG ?= latest NPM_RELEASE_TAG ?= latest
EXTENSIONS_DIR = ./extensions EXTENSIONS_DIR = ./extensions
extensions = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir}) extensions = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir})
@ -64,7 +65,7 @@ ifeq "$(DETECTED_OS)" "Windows"
rm -rf node_modules/win-ca/pem rm -rf node_modules/win-ca/pem
yarn run electron-builder --publish onTag --x64 --ia32 yarn run electron-builder --publish onTag --x64 --ia32
else else
yarn run electron-builder --publish onTag yarn run electron-builder --publish onTag --$(BINARY_ARCH)
endif endif
$(extension_node_modules): node_modules $(extension_node_modules): node_modules

View File

@ -25,7 +25,8 @@ 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";
import { noop } from "../src/common/utils"; import { noop } from "lodash";
import { isLinux, isMac } from "../src/common/vars";
class KubectlDownloader { class KubectlDownloader {
public kubectlVersion: string; public kubectlVersion: string;
@ -115,15 +116,21 @@ class KubectlDownloader {
}); });
} }
} }
const downloadVersion = packageInfo.config.bundledKubectlVersion; const downloadVersion = packageInfo.config.bundledKubectlVersion;
const baseDir = path.join(process.env.INIT_CWD, "binaries", "client"); const baseDir = path.join(__dirname, "..", "binaries", "client");
const downloads = [ const binaryArch = process.env.BINARY_ARCH || "amd64";
{ platform: "linux", arch: "amd64", target: path.join(baseDir, "linux", "x64", "kubectl") }, const binaryTargetArch = binaryArch === "amd64" ? "x64" : "arm64";
{ platform: "darwin", arch: "amd64", target: path.join(baseDir, "darwin", "x64", "kubectl") },
{ platform: "windows", arch: "amd64", target: path.join(baseDir, "windows", "x64", "kubectl.exe") }, const downloads = [];
{ platform: "windows", arch: "386", target: path.join(baseDir, "windows", "ia32", "kubectl.exe") }
]; if (isMac) {
downloads.push({ platform: "darwin", arch: binaryArch, target: path.join(baseDir, "darwin", binaryTargetArch, "kubectl") });
} else if (isLinux) {
downloads.push({ platform: "linux", arch: binaryArch, target: path.join(baseDir, "linux", binaryTargetArch, "kubectl") });
} else {
downloads.push({ platform: "windows", arch: "amd64", target: path.join(baseDir, "windows", binaryTargetArch, "kubectl.exe") });
downloads.push({ platform: "windows", arch: "386", target: path.join(baseDir, "windows", "ia32", "kubectl.exe") });
}
downloads.forEach((dlOpts) => { downloads.forEach((dlOpts) => {
console.log(dlOpts); console.log(dlOpts);

View File

@ -0,0 +1,74 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE
namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
cluster and vice versa.
*/
import type { Page } from "playwright";
import * as utils from "../helpers/utils";
describe("preferences page tests", () => {
let window: Page, cleanup: () => Promise<void>;
beforeEach(async () => {
({ window, cleanup } = await utils.start());
await utils.clickWelcomeButton(window);
await window.keyboard.press("Meta+,");
}, 10*60*1000);
afterEach(async () => {
await cleanup();
}, 10*60*1000);
it('shows "preferences" and can navigate through the tabs', async () => {
const pages = [
{
id: "application",
header: "Application",
},
{
id: "proxy",
header: "Proxy",
},
{
id: "kubernetes",
header: "Kubernetes",
},
];
for (const { id, header } of pages) {
await window.click(`[data-testid=${id}-tab]`);
await window.waitForSelector(`[data-testid=${id}-header] >> text=${header}`);
}
}, 10*60*1000);
utils.itIf(process.platform !== "win32")("ensures helm repos", async () => {
await window.click("[data-testid=kubernetes-tab]");
await window.waitForSelector("[data-testid=repository-name]", {
timeout: 100_000,
});
await window.click("#HelmRepoSelect");
await window.waitForSelector("div.Select__option");
}, 10*60*1000);
});

View File

@ -1,87 +0,0 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE
namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
cluster and vice versa.
*/
import type { Application } from "spectron";
import * as utils from "../helpers/utils";
import { listHelmRepositories } from "../helpers/utils";
import { fail } from "assert";
jest.setTimeout(2 * 60 * 1000); // 2 minutes so that we can get better errors from spectron
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
describe("Lens integration tests", () => {
let app: Application;
describe("app start", () => {
utils.beforeAllWrapped(async () => {
app = await utils.setup();
});
utils.afterAllWrapped(() => utils.tearDown(app));
it('shows "add cluster"', async () => {
await app.electron.ipcRenderer.send("test-menu-item-click", "File", "Add Cluster");
await app.client.waitUntilTextExists("h2", "Add Clusters from Kubeconfig");
});
describe("preferences page", () => {
it('shows "preferences"', async () => {
const appName: string = process.platform === "darwin" ? "OpenLens" : "File";
await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences");
await app.client.waitUntilTextExists("[data-testid=application-header]", "Application");
});
it.each([
["application", "Application"],
["proxy", "Proxy"],
["kubernetes", "Kubernetes"],
])("Can click the %s tab and see the %s header", async (tab, header) => {
await app.client.click(`[data-testid=${tab}-tab]`);
await app.client.waitUntilTextExists(`[data-testid=${tab}-header]`, header);
});
it("ensures helm repos", async () => {
const repos = await listHelmRepositories();
if (repos.length === 0) {
fail("Lens failed to add any repositories");
}
await app.client.click("[data-testid=kubernetes-tab]");
await app.client.waitUntilTextExists("[data-testid=repository-name]", repos[0].name); // wait for the helm-cli to fetch the repo(s)
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)
});
});
it.skip('quits Lens"', async () => {
await app.client.keys(["Meta", "Q"]);
await app.client.keys("Meta");
});
});
});

View File

@ -25,450 +25,439 @@
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 type { Application } from "spectron";
import * as utils from "../helpers/utils"; import * as utils from "../helpers/utils";
import { minikubeReady, waitForMinikubeDashboard } from "../helpers/minikube"; import { minikubeReady } from "../helpers/minikube";
import { exec } from "child_process"; import type { Frame, Page } from "playwright";
import * as util from "util";
export const promiseExec = util.promisify(exec); const TEST_NAMESPACE = "integration-tests";
jest.setTimeout(2 * 60 * 1000); // 2 minutes so that we can get better errors from spectron function getSidebarSelectors(itemId: string) {
const root = `.SidebarItem[data-test-id="${itemId}"]`;
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) return {
describe("Lens cluster pages", () => { expandSubMenu: `${root} .nav-item`,
const TEST_NAMESPACE = "integration-tests"; subMenuLink: (href: string) => `.Sidebar .sub-menu a[href^="/${href}"]`,
const BACKSPACE = "\uE003"; };
let app: Application; }
const ready = minikubeReady(TEST_NAMESPACE);
utils.describeIf(ready)("test common pages", () => { function getLoadedSelector(page: CommonPage): string {
let clusterAdded = false; if (page.expectedText) {
const addCluster = async () => { return `${page.expectedSelector} >> text='${page.expectedText}'`;
await app.client.waitUntilTextExists("div", "Catalog"); }
await waitForMinikubeDashboard(app);
await app.client.click('a[href="/nodes"]');
await app.client.waitUntilTextExists("div.TableCell", "Ready");
};
describe("cluster add", () => { return page.expectedSelector;
utils.beforeAllWrapped(async () => { }
app = await utils.setup();
});
utils.afterAllWrapped(() => utils.tearDown(app)); interface CommonPage {
name: string;
href: string;
expectedSelector: string;
expectedText?: string;
}
it("allows to add a cluster", async () => { interface TopPageTest {
await addCluster(); page: CommonPage;
clusterAdded = true; }
});
});
const appStartAddCluster = async () => { interface SubPageTest {
if (clusterAdded) { drawerId: string;
app = await utils.setup(); pages: CommonPage[];
await addCluster(); }
type CommonPageTest = TopPageTest | SubPageTest;
function isTopPageTest(test: CommonPageTest): test is TopPageTest {
return typeof (test as any).page === "object";
}
const commonPageTests: CommonPageTest[] = [{
page: {
name: "Cluster",
href: "cluster",
expectedSelector: "div.ClusterOverview div.label",
expectedText: "CPU"
}
},
{
page: {
name: "Nodes",
href: "nodes",
expectedSelector: "h5.title",
expectedText: "Nodes"
}
},
{
drawerId: "workloads",
pages: [{
name: "Overview",
href: "workloads",
expectedSelector: "h5.box",
expectedText: "Overview"
},
{
name: "Pods",
href: "pods",
expectedSelector: "h5.title",
expectedText: "Pods"
},
{
name: "Deployments",
href: "deployments",
expectedSelector: "h5.title",
expectedText: "Deployments"
},
{
name: "DaemonSets",
href: "daemonsets",
expectedSelector: "h5.title",
expectedText: "Daemon Sets"
},
{
name: "StatefulSets",
href: "statefulsets",
expectedSelector: "h5.title",
expectedText: "Stateful Sets"
},
{
name: "ReplicaSets",
href: "replicasets",
expectedSelector: "h5.title",
expectedText: "Replica Sets"
},
{
name: "Jobs",
href: "jobs",
expectedSelector: "h5.title",
expectedText: "Jobs"
},
{
name: "CronJobs",
href: "cronjobs",
expectedSelector: "h5.title",
expectedText: "Cron Jobs"
}]
},
{
drawerId: "config",
pages: [{
name: "ConfigMaps",
href: "configmaps",
expectedSelector: "h5.title",
expectedText: "Config Maps"
},
{
name: "Secrets",
href: "secrets",
expectedSelector: "h5.title",
expectedText: "Secrets"
},
{
name: "Resource Quotas",
href: "resourcequotas",
expectedSelector: "h5.title",
expectedText: "Resource Quotas"
},
{
name: "Limit Ranges",
href: "limitranges",
expectedSelector: "h5.title",
expectedText: "Limit Ranges"
},
{
name: "HPA",
href: "hpa",
expectedSelector: "h5.title",
expectedText: "Horizontal Pod Autoscalers"
},
{
name: "Pod Disruption Budgets",
href: "poddisruptionbudgets",
expectedSelector: "h5.title",
expectedText: "Pod Disruption Budgets"
}]
},
{
drawerId: "networks",
pages: [{
name: "Services",
href: "services",
expectedSelector: "h5.title",
expectedText: "Services"
},
{
name: "Endpoints",
href: "endpoints",
expectedSelector: "h5.title",
expectedText: "Endpoints"
},
{
name: "Ingresses",
href: "ingresses",
expectedSelector: "h5.title",
expectedText: "Ingresses"
},
{
name: "Network Policies",
href: "network-policies",
expectedSelector: "h5.title",
expectedText: "Network Policies"
}]
},
{
drawerId: "storage",
pages: [{
name: "Persistent Volume Claims",
href: "persistent-volume-claims",
expectedSelector: "h5.title",
expectedText: "Persistent Volume Claims"
},
{
name: "Persistent Volumes",
href: "persistent-volumes",
expectedSelector: "h5.title",
expectedText: "Persistent Volumes"
},
{
name: "Storage Classes",
href: "storage-classes",
expectedSelector: "h5.title",
expectedText: "Storage Classes"
}]
},
{
page: {
name: "Namespaces",
href: "namespaces",
expectedSelector: "h5.title",
expectedText: "Namespaces"
}
},
{
page: {
name: "Events",
href: "events",
expectedSelector: "h5.title",
expectedText: "Events"
}
},
{
drawerId: "apps",
pages: [{
name: "Charts",
href: "apps/charts",
expectedSelector: "div.HelmCharts input",
},
{
name: "Releases",
href: "apps/releases",
expectedSelector: "h5.title",
expectedText: "Releases"
}]
},
{
drawerId: "users",
pages: [{
name: "Service Accounts",
href: "service-accounts",
expectedSelector: "h5.title",
expectedText: "Service Accounts"
},
{
name: "Roles",
href: "roles",
expectedSelector: "h5.title",
expectedText: "Roles"
},
{
name: "Cluster Roles",
href: "cluster-roles",
expectedSelector: "h5.title",
expectedText: "Cluster Roles"
},
{
name: "Role Bindings",
href: "role-bindings",
expectedSelector: "h5.title",
expectedText: "Role Bindings"
},
{
name: "Cluster Role Bindings",
href: "cluster-role-bindings",
expectedSelector: "h5.title",
expectedText: "Cluster Role Bindings"
},
{
name: "Pod Security Policies",
href: "pod-security-policies",
expectedSelector: "h5.title",
expectedText: "Pod Security Policies"
}]
},
{
drawerId: "custom-resources",
pages: [{
name: "Definitions",
href: "crd/definitions",
expectedSelector: "h5.title",
expectedText: "Custom Resources"
}]
}];
utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
let window: Page, cleanup: () => Promise<void>, frame: Frame;
beforeEach(async () => {
({ window, cleanup } = await utils.start());
await utils.clickWelcomeButton(window);
frame = await utils.lauchMinikubeClusterFromCatalog(window);
}, 10*60*1000);
afterEach(async () => {
await cleanup();
}, 10*60*1000);
it("should navigate around common cluster pages", async () => {
for (const test of commonPageTests) {
if (isTopPageTest(test)) {
const { href, expectedText, expectedSelector } = test.page;
const menuButton = await frame.waitForSelector(`a[href^="/${href}"]`);
await menuButton.click();
await frame.waitForSelector(`${expectedSelector} >> text='${expectedText}'`);
continue;
} }
};
function getSidebarSelectors(itemId: string) { const { drawerId, pages } = test;
const root = `.SidebarItem[data-test-id="${itemId}"]`; const selectors = getSidebarSelectors(drawerId);
const mainPageSelector = `${selectors.subMenuLink(pages[0].href)} >> text='${pages[0].name}'`;
return { await frame.click(selectors.expandSubMenu);
expandSubMenu: `${root} .nav-item`, await frame.waitForSelector(mainPageSelector);
subMenuLink: (href: string) => `.Sidebar .sub-menu a[href^="/${href}"]`,
}; for (const page of pages) {
const subPageButton = await frame.waitForSelector(selectors.subMenuLink(page.href));
await subPageButton.click();
await frame.waitForSelector(getLoadedSelector(page));
}
await frame.click(selectors.expandSubMenu);
await frame.waitForSelector(mainPageSelector, { state: "hidden" });
}
}, 10*60*1000);
it("show logs and highlight the log search entries", async () => {
await frame.click(`a[href="/workloads"]`);
await frame.click(`a[href="/pods"]`);
const namespacesSelector = await frame.waitForSelector(".NamespaceSelect");
await namespacesSelector.click();
await namespacesSelector.type("kube-system");
await namespacesSelector.press("Enter");
await namespacesSelector.click();
const kubeApiServerRow = await frame.waitForSelector("div.TableCell >> text=kube-apiserver");
await kubeApiServerRow.click();
await frame.waitForSelector(".Drawer", { state: "visible" });
const logButton = await frame.waitForSelector("ul.KubeObjectMenu li.MenuItem i.Icon span[data-icon-name='subject']");
await logButton.click();
// Check if controls are available
await frame.waitForSelector(".LogList .VirtualList");
await frame.waitForSelector(".LogResourceSelector");
const logSearchInput = await frame.waitForSelector(".LogSearch .SearchInput input");
await logSearchInput.type(":");
await frame.waitForSelector(".LogList .list span.active");
const showTimestampsButton = await frame.waitForSelector(".LogControls .show-timestamps");
await showTimestampsButton.click();
const showPreviousButton = await frame.waitForSelector(".LogControls .show-previous");
await showPreviousButton.click();
}, 10*60*1000);
it("should show the default namespaces", async () => {
await frame.click('a[href="/namespaces"]');
await frame.waitForSelector("div.TableCell >> text='default'");
await frame.waitForSelector("div.TableCell >> text='kube-system'");
}, 10*60*1000);
it(`should create the ${TEST_NAMESPACE} and a pod in the namespace`, async () => {
await frame.click('a[href="/namespaces"]');
await frame.click("button.add-button");
await frame.waitForSelector("div.AddNamespaceDialog >> text='Create Namespace'");
const namespaceNameInput = await frame.waitForSelector(".AddNamespaceDialog input");
await namespaceNameInput.type(TEST_NAMESPACE);
await namespaceNameInput.press("Enter");
await frame.waitForSelector(`div.TableCell >> text=${TEST_NAMESPACE}`);
if ((await frame.innerText(`a[href^="/workloads"] .expand-icon`)) === "keyboard_arrow_down") {
await frame.click(`a[href^="/workloads"]`);
} }
describe("cluster pages", () => { await frame.click(`a[href^="/pods"]`);
utils.beforeAllWrapped(appStartAddCluster);
utils.afterAllWrapped(() => utils.tearDown(app));
const tests: { const namespacesSelector = await frame.waitForSelector(".NamespaceSelect");
drawer?: string
drawerId?: string
pages: {
name: string,
href: string,
expectedSelector: string,
expectedText: string
}[]
}[] = [{
drawer: "",
drawerId: "",
pages: [{
name: "Cluster",
href: "cluster",
expectedSelector: "div.ClusterOverview div.label",
expectedText: "CPU"
}]
},
{
drawer: "",
drawerId: "",
pages: [{
name: "Nodes",
href: "nodes",
expectedSelector: "h5.title",
expectedText: "Nodes"
}]
},
{
drawer: "Workloads",
drawerId: "workloads",
pages: [{
name: "Overview",
href: "workloads",
expectedSelector: "h5.box",
expectedText: "Overview"
},
{
name: "Pods",
href: "pods",
expectedSelector: "h5.title",
expectedText: "Pods"
},
{
name: "Deployments",
href: "deployments",
expectedSelector: "h5.title",
expectedText: "Deployments"
},
{
name: "DaemonSets",
href: "daemonsets",
expectedSelector: "h5.title",
expectedText: "Daemon Sets"
},
{
name: "StatefulSets",
href: "statefulsets",
expectedSelector: "h5.title",
expectedText: "Stateful Sets"
},
{
name: "ReplicaSets",
href: "replicasets",
expectedSelector: "h5.title",
expectedText: "Replica Sets"
},
{
name: "Jobs",
href: "jobs",
expectedSelector: "h5.title",
expectedText: "Jobs"
},
{
name: "CronJobs",
href: "cronjobs",
expectedSelector: "h5.title",
expectedText: "Cron Jobs"
}]
},
{
drawer: "Configuration",
drawerId: "config",
pages: [{
name: "ConfigMaps",
href: "configmaps",
expectedSelector: "h5.title",
expectedText: "Config Maps"
},
{
name: "Secrets",
href: "secrets",
expectedSelector: "h5.title",
expectedText: "Secrets"
},
{
name: "Resource Quotas",
href: "resourcequotas",
expectedSelector: "h5.title",
expectedText: "Resource Quotas"
},
{
name: "Limit Ranges",
href: "limitranges",
expectedSelector: "h5.title",
expectedText: "Limit Ranges"
},
{
name: "HPA",
href: "hpa",
expectedSelector: "h5.title",
expectedText: "Horizontal Pod Autoscalers"
},
{
name: "Pod Disruption Budgets",
href: "poddisruptionbudgets",
expectedSelector: "h5.title",
expectedText: "Pod Disruption Budgets"
}]
},
{
drawer: "Network",
drawerId: "networks",
pages: [{
name: "Services",
href: "services",
expectedSelector: "h5.title",
expectedText: "Services"
},
{
name: "Endpoints",
href: "endpoints",
expectedSelector: "h5.title",
expectedText: "Endpoints"
},
{
name: "Ingresses",
href: "ingresses",
expectedSelector: "h5.title",
expectedText: "Ingresses"
},
{
name: "Network Policies",
href: "network-policies",
expectedSelector: "h5.title",
expectedText: "Network Policies"
}]
},
{
drawer: "Storage",
drawerId: "storage",
pages: [{
name: "Persistent Volume Claims",
href: "persistent-volume-claims",
expectedSelector: "h5.title",
expectedText: "Persistent Volume Claims"
},
{
name: "Persistent Volumes",
href: "persistent-volumes",
expectedSelector: "h5.title",
expectedText: "Persistent Volumes"
},
{
name: "Storage Classes",
href: "storage-classes",
expectedSelector: "h5.title",
expectedText: "Storage Classes"
}]
},
{
drawer: "",
drawerId: "",
pages: [{
name: "Namespaces",
href: "namespaces",
expectedSelector: "h5.title",
expectedText: "Namespaces"
}]
},
{
drawer: "",
drawerId: "",
pages: [{
name: "Events",
href: "events",
expectedSelector: "h5.title",
expectedText: "Events"
}]
},
{
drawer: "Apps",
drawerId: "apps",
pages: [{
name: "Charts",
href: "apps/charts",
expectedSelector: "div.HelmCharts input",
expectedText: ""
},
{
name: "Releases",
href: "apps/releases",
expectedSelector: "h5.title",
expectedText: "Releases"
}]
},
{
drawer: "Access Control",
drawerId: "users",
pages: [{
name: "Service Accounts",
href: "service-accounts",
expectedSelector: "h5.title",
expectedText: "Service Accounts"
},
{
name: "Role Bindings",
href: "role-bindings",
expectedSelector: "h5.title",
expectedText: "Role Bindings"
},
{
name: "Roles",
href: "roles",
expectedSelector: "h5.title",
expectedText: "Roles"
},
{
name: "Pod Security Policies",
href: "pod-security-policies",
expectedSelector: "h5.title",
expectedText: "Pod Security Policies"
}]
},
{
drawer: "Custom Resources",
drawerId: "custom-resources",
pages: [{
name: "Definitions",
href: "crd/definitions",
expectedSelector: "h5.title",
expectedText: "Custom Resources"
}]
}];
tests.forEach(({ drawer = "", drawerId = "", pages }) => { await namespacesSelector.click();
const selectors = getSidebarSelectors(drawerId); await namespacesSelector.type(TEST_NAMESPACE);
await namespacesSelector.press("Enter");
await namespacesSelector.click();
if (drawer !== "") { await frame.click(".Icon.new-dock-tab");
it(`shows ${drawer} drawer`, async () => {
expect(clusterAdded).toBe(true);
await app.client.click(selectors.expandSubMenu);
await app.client.waitUntilTextExists(selectors.subMenuLink(pages[0].href), pages[0].name);
});
pages.forEach(({ name, href, expectedSelector, expectedText }) => { try {
it(`shows ${drawer}->${name} page`, async () => { await frame.click("li.MenuItem.create-resource-tab", {
expect(clusterAdded).toBe(true); // NOTE: the following shouldn't be required, but is because without it a TypeError is thrown
await app.client.click(selectors.subMenuLink(href)); // see: https://github.com/microsoft/playwright/issues/8229
await app.client.waitUntilTextExists(expectedSelector, expectedText); position: {
}); y: 0,
}); x: 0,
it(`hides ${drawer} drawer`, async () => {
expect(clusterAdded).toBe(true);
await app.client.click(selectors.expandSubMenu);
await expect(app.client.waitUntilTextExists(selectors.subMenuLink(pages[0].href), pages[0].name, 100)).rejects.toThrow();
});
} else {
const { href, name, expectedText, expectedSelector } = pages[0];
it(`shows page ${name}`, async () => {
expect(clusterAdded).toBe(true);
await app.client.click(`a[href^="/${href}"]`);
await app.client.waitUntilTextExists(expectedSelector, expectedText);
});
} }
}); });
}); } catch (error) {
console.log(error);
await frame.waitForTimeout(100_000);
}
describe("viewing pod logs", () => { const inputField = await frame.waitForSelector(".CreateResource div.react-monaco-editor-container");
utils.beforeEachWrapped(appStartAddCluster);
utils.afterEachWrapped(() => utils.tearDown(app));
it(`shows a log for a pod`, async () => { await inputField.click();
expect(clusterAdded).toBe(true); await inputField.type("apiVersion: v1", { delay: 10 });
// Go to Pods page await inputField.press("Enter", { delay: 10 });
await app.client.click(getSidebarSelectors("workloads").expandSubMenu); await inputField.type("kind: Pod", { delay: 10 });
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); await inputField.press("Enter", { delay: 10 });
await app.client.click('a[href^="/pods"]'); await inputField.type("metadata:", { delay: 10 });
await app.client.click(".NamespaceSelect"); await inputField.press("Enter", { delay: 10 });
await app.client.keys("kube-system"); await inputField.type(" name: nginx-create-pod-test", { delay: 10 });
await app.client.keys("Enter");// "\uE007" await inputField.press("Enter", { delay: 10 });
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); await inputField.type(`namespace: ${TEST_NAMESPACE}`, { delay: 10 });
let podMenuItemEnabled = false; await inputField.press("Enter", { delay: 10 });
await inputField.press("Backspace", { delay: 10 });
await inputField.type("spec:", { delay: 10 });
await inputField.press("Enter", { delay: 10 });
await inputField.type(" containers:", { delay: 10 });
await inputField.press("Enter", { delay: 10 });
await inputField.type("- name: nginx-create-pod-test", { delay: 10 });
await inputField.press("Enter", { delay: 10 });
await inputField.type(" image: nginx:alpine", { delay: 10 });
await inputField.press("Enter", { delay: 10 });
// Wait until extensions are enabled on renderer await frame.click("button.Button >> text='Create & Close'");
while (!podMenuItemEnabled) { await frame.click("div.TableCell >> text=nginx-create-pod-test");
const logs = await app.client.getRenderProcessLogs(); await frame.waitForSelector("div.drawer-title-text >> text='Pod: nginx-create-pod-test'");
}, 10*60*1000);
podMenuItemEnabled = !!logs.find(entry => entry.message.includes("[EXTENSION]: enabled lens-pod-menu@"));
if (!podMenuItemEnabled) {
await new Promise(r => setTimeout(r, 1000));
}
}
// Open logs tab in dock
await app.client.click(".list .TableRow:first-child");
await app.client.waitForVisible(".Drawer");
const logsButton = "ul.KubeObjectMenu li.MenuItem i.Icon span[data-icon-name='subject']";
await app.client.waitForVisible(logsButton);
await app.client.click(logsButton);
// Check if controls are available
await app.client.waitForVisible(".LogList .VirtualList");
await app.client.waitForVisible(".LogResourceSelector");
//await app.client.waitForVisible(".LogSearch .SearchInput");
await app.client.waitForVisible(".LogSearch .SearchInput input");
// Search for semicolon
await app.client.keys(":");
await app.client.waitForVisible(".LogList .list span.active");
// Click through controls
await app.client.click(".LogControls .show-timestamps");
await app.client.click(".LogControls .show-previous");
});
});
describe("cluster operations", () => {
utils.beforeEachWrapped(appStartAddCluster);
utils.afterEachWrapped(() => utils.tearDown(app));
it("shows default namespace", async () => {
expect(clusterAdded).toBe(true);
await app.client.click('a[href="/namespaces"]');
await app.client.waitUntilTextExists("div.TableCell", "default");
await app.client.waitUntilTextExists("div.TableCell", "kube-system");
});
it(`creates ${TEST_NAMESPACE} namespace`, async () => {
expect(clusterAdded).toBe(true);
await app.client.click('a[href="/namespaces"]');
await app.client.waitUntilTextExists("div.TableCell", "default");
await app.client.waitUntilTextExists("div.TableCell", "kube-system");
await app.client.click("button.add-button");
await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace");
await app.client.keys(`${TEST_NAMESPACE}\n`);
await app.client.waitForExist(`.name=${TEST_NAMESPACE}`);
});
it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => {
expect(clusterAdded).toBe(true);
await app.client.click(getSidebarSelectors("workloads").expandSubMenu);
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
await app.client.click('a[href^="/pods"]');
await app.client.click(".NamespaceSelect");
await app.client.keys(TEST_NAMESPACE);
await app.client.keys("Enter");// "\uE007"
await app.client.click(".Icon.new-dock-tab");
await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource");
await app.client.click("li.MenuItem.create-resource-tab");
await app.client.waitForVisible(".CreateResource div.react-monaco-editor-container");
// Write pod manifest to editor
await app.client.click(".CreateResource div.react-monaco-editor-container");
await app.client.keys("apiVersion: v1\n");
await app.client.keys("kind: Pod\n");
await app.client.keys("metadata:\n");
await app.client.keys(" name: nginx-create-pod-test\n");
await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`);
await app.client.keys(`${BACKSPACE}spec:\n`);
await app.client.keys(" containers:\n");
await app.client.keys("- name: nginx-create-pod-test\n");
await app.client.keys(" image: nginx:alpine\n");
// Create deployment
await app.client.waitForEnabled("button.Button=Create & Close");
await app.client.click("button.Button=Create & Close");
// Wait until first bits of pod appears on dashboard
await app.client.waitForExist(".name=nginx-create-pod-test");
// Open pod details
await app.client.click(".name=nginx-create-pod-test");
await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test");
});
});
});
}); });

View File

@ -19,25 +19,25 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { Application } from "spectron"; import type { Page } from "playwright";
import * as utils from "../helpers/utils"; import * as utils from "../helpers/utils";
jest.setTimeout(2 * 60 * 1000); // 2 minutes so that we can get better errors from spectron
describe("Lens command palette", () => { describe("Lens command palette", () => {
let app: Application; let window: Page, cleanup: () => Promise<void>;
beforeEach(async () => {
({ window, cleanup } = await utils.start());
await utils.clickWelcomeButton(window);
}, 10*60*1000);
afterEach(async () => {
await cleanup();
}, 10*60*1000);
describe("menu", () => { describe("menu", () => {
utils.beforeAllWrapped(async () => { it("opens command dialog from keyboard shortcut", async () => {
app = await utils.setup(); await window.keyboard.press("Meta+Shift+p");
}); await window.waitForSelector(".Select__option >> text=Hotbar: Switch");
}, 10*60*1000);
utils.afterAllWrapped(() => utils.tearDown(app));
it("opens command dialog from menu", async () => {
await app.electron.ipcRenderer.send("test-menu-item-click", "View", "Command Palette...");
await app.client.waitUntilTextExists(".Select__option", "Hotbar: Switch");
await app.client.keys("Escape");
});
}); });
}); });

View File

@ -19,7 +19,6 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { spawnSync } from "child_process"; import { spawnSync } from "child_process";
import type { Application } from "spectron";
export function minikubeReady(testNamespace: string): boolean { export function minikubeReady(testNamespace: string): boolean {
// determine if minikube is running // determine if minikube is running
@ -57,18 +56,3 @@ export function minikubeReady(testNamespace: string): boolean {
return true; return true;
} }
export async function waitForMinikubeDashboard(app: Application) {
await app.client.waitUntilTextExists("div.TableCell", "minikube");
await app.client.waitForExist(".Input.SearchInput input");
await app.client.setValue(".Input.SearchInput input", "minikube");
await app.client.waitUntilTextExists("div.TableCell", "minikube");
await app.client.click("div.TableRow");
await app.client.waitUntilTextExists("div.drawer-title-text", "KubernetesCluster: minikube");
await app.client.waitForExist("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
await app.client.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
await app.client.waitForExist(`iframe[name="minikube"]`);
await app.client.frame("minikube");
await app.client.waitUntilTextExists("span.link-text", "Cluster");
}

View File

@ -18,49 +18,20 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { Application } from "spectron"; import { createHash } from "crypto";
import * as util from "util"; import { mkdirp, remove } from "fs-extra";
import { exec } from "child_process"; import * as os from "os";
import * as path from "path";
import * as uuid from "uuid";
import { ElectronApplication, Frame, Page, _electron as electron } from "playwright";
import { noop } from "lodash";
const AppPaths: Partial<Record<NodeJS.Platform, string>> = { export const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
"win32": "./dist/win-unpacked/OpenLens.exe", "win32": "./dist/win-unpacked/OpenLens.exe",
"linux": "./dist/linux-unpacked/open-lens", "linux": "./dist/linux-unpacked/open-lens",
"darwin": "./dist/mac/OpenLens.app/Contents/MacOS/OpenLens", "darwin": "./dist/mac/OpenLens.app/Contents/MacOS/OpenLens",
}; };
interface DoneCallback {
(...args: any[]): any;
fail(error?: string | { message: string }): any;
}
/**
* This is necessary because Jest doesn't do this correctly.
* @param fn The function to call
*/
export function wrapJestLifecycle(fn: () => Promise<void> | void): (done: DoneCallback) => void {
return function (done: DoneCallback) {
(async () => fn())()
.then(() => done())
.catch(error => done.fail(error));
};
}
export function beforeAllWrapped(fn: () => Promise<void> | void): void {
beforeAll(wrapJestLifecycle(fn));
}
export function beforeEachWrapped(fn: () => Promise<void> | void): void {
beforeEach(wrapJestLifecycle(fn));
}
export function afterAllWrapped(fn: () => Promise<void> | void): void {
afterAll(wrapJestLifecycle(fn));
}
export function afterEachWrapped(fn: () => Promise<void> | void): void {
afterEach(wrapJestLifecycle(fn));
}
export function itIf(condition: boolean) { export function itIf(condition: boolean) {
return condition ? it : it.skip; return condition ? it : it.skip;
} }
@ -69,71 +40,80 @@ export function describeIf(condition: boolean) {
return condition ? describe : describe.skip; return condition ? describe : describe.skip;
} }
export const keys = { async function getMainWindow(app: ElectronApplication, timeout = 50_000): Promise<Page> {
backspace: "\uE003" const deadline = Date.now() + timeout;
};
export async function setup(): Promise<Application> { for (; Date.now() < deadline;) {
const app = new Application({ for (const page of app.windows()) {
path: AppPaths[process.platform], // path to electron app if (page.url().startsWith("http://localhost")) {
args: [], return page;
startTimeout: 60000, }
waitTimeout: 10000,
env: {
CICD: "true"
} }
});
await app.start(); await new Promise(resolve => setTimeout(resolve, 2_000));
// Wait for splash screen to be closed
while (await app.client.getWindowCount() > 1);
await app.client.windowByIndex(0);
await app.client.waitUntilWindowLoaded();
await showCatalog(app);
return app;
}
export async function showCatalog(app: Application) {
await app.client.waitForExist("#hotbarIcon-catalog-entity .Icon");
await app.client.click("#hotbarIcon-catalog-entity .Icon");
}
type AsyncPidGetter = () => Promise<number>;
export async function tearDown(app?: Application) {
if (!app?.isRunning()) {
return;
} }
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)(); throw new Error(`Lens did not open the main window within ${timeout}ms`);
}
await app.stop(); export async function start() {
const CICD = path.join(os.tmpdir(), "lens-integration-testing", uuid.v4());
// Make sure that the directory is clear
await remove(CICD).catch(noop);
await mkdirp(CICD);
const app = await electron.launch({
args: ["--integration-testing"], // this argument turns off the blocking of quit
executablePath: AppPaths[process.platform],
bypassCSP: true,
env: {
CICD,
...process.env
},
timeout: 100_000,
} as Parameters<typeof electron["launch"]>[0]);
try { try {
process.kill(pid, "SIGKILL"); const window = await getMainWindow(app);
} catch (e) {
console.error(e); return {
app,
window,
cleanup: async () => {
await app.close();
await remove(CICD).catch(noop);
},
};
} catch (error) {
await app.close();
await remove(CICD).catch(noop);
throw error;
} }
} }
export const promiseExec = util.promisify(exec); export async function clickWelcomeButton(window: Page) {
await window.click("#hotbarIcon-catalog-entity .Icon");
type HelmRepository = { }
name: string;
url: string; function minikubeEntityId() {
}; return createHash("md5").update(`${path.join(os.homedir(), ".kube", "config")}:minikube`).digest("hex");
}
export async function listHelmRepositories(): Promise<HelmRepository[]>{
for (let i = 0; i < 10; i += 1) { /**
try { * From the catalog, click the minikube entity and wait for it to connect, returning its frame
const { stdout } = await promiseExec("helm repo list -o json"); */
export async function lauchMinikubeClusterFromCatalog(window: Page): Promise<Frame> {
return JSON.parse(stdout); await window.waitForSelector("div.TableCell");
} catch { await window.click("div.TableCell >> text='minikube'");
await new Promise(r => setTimeout(r, 2000)); // if no repositories, wait for Lens adding bitnami repository await window.waitForSelector("div.drawer-title-text >> text='KubernetesCluster: minikube'");
} await window.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
}
const minikubeFrame = await window.waitForSelector(`#cluster-frame-${minikubeEntityId()}`);
return [];
const frame = await minikubeFrame.contentFrame();
await frame.waitForSelector("div.Sidebar");
return frame;
} }

View File

@ -28,9 +28,8 @@
"build:linux": "yarn run compile && electron-builder --linux --dir", "build:linux": "yarn run compile && electron-builder --linux --dir",
"build:mac": "yarn run compile && electron-builder --mac --dir", "build:mac": "yarn run compile && electron-builder --mac --dir",
"build:win": "yarn run compile && electron-builder --win --dir", "build:win": "yarn run compile && electron-builder --win --dir",
"integration": "jest --runInBand integration", "integration": "jest --runInBand --detectOpenHandles --forceExit integration",
"dist": "yarn run compile && electron-builder --publish onTag", "dist": "yarn run compile && electron-builder --publish onTag",
"dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32",
"dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null", "dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null",
"download-bins": "concurrently yarn:download:*", "download-bins": "concurrently yarn:download:*",
"download:kubectl": "yarn run ts-node build/download_kubectl.ts", "download:kubectl": "yarn run ts-node build/download_kubectl.ts",
@ -48,11 +47,11 @@
}, },
"config": { "config": {
"bundledKubectlVersion": "1.21.2", "bundledKubectlVersion": "1.21.2",
"bundledHelmVersion": "3.5.4", "bundledHelmVersion": "3.6.3",
"sentryDsn": "" "sentryDsn": ""
}, },
"engines": { "engines": {
"node": ">=12 <13" "node": ">=14 <15"
}, },
"jest": { "jest": {
"collectCoverage": false, "collectCoverage": false,
@ -141,6 +140,10 @@
"from": "binaries/client/darwin/x64/kubectl", "from": "binaries/client/darwin/x64/kubectl",
"to": "./x64/kubectl" "to": "./x64/kubectl"
}, },
{
"from": "binaries/client/darwin/arm64/kubectl",
"to": "./arm64/kubectl"
},
{ {
"from": "binaries/client/helm3/helm3", "from": "binaries/client/helm3/helm3",
"to": "./helm3/helm3" "to": "./helm3/helm3"
@ -180,6 +183,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@electron/remote": "^1.2.1",
"@hapi/call": "^8.0.1", "@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3", "@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.15.1", "@kubernetes/client-node": "^0.15.1",
@ -263,7 +267,7 @@
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.6", "@testing-library/react": "^11.2.6",
"@types/byline": "^4.2.32", "@types/byline": "^4.2.32",
"@types/chart.js": "^2.9.21", "@types/chart.js": "^2.9.34",
"@types/color": "^3.0.2", "@types/color": "^3.0.2",
"@types/crypto-js": "^3.1.47", "@types/crypto-js": "^3.1.47",
"@types/dompurify": "^2.0.2", "@types/dompurify": "^2.0.2",
@ -287,7 +291,7 @@
"@types/module-alias": "^2.0.0", "@types/module-alias": "^2.0.0",
"@types/node": "12.20", "@types/node": "12.20",
"@types/node-fetch": "^2.5.12", "@types/node-fetch": "^2.5.12",
"@types/npm": "^2.0.31", "@types/npm": "^2.0.32",
"@types/progress-bar-webpack-plugin": "^2.1.2", "@types/progress-bar-webpack-plugin": "^2.1.2",
"@types/proper-lockfile": "^4.1.1", "@types/proper-lockfile": "^4.1.1",
"@types/randomcolor": "^0.5.6", "@types/randomcolor": "^0.5.6",
@ -326,7 +330,7 @@
"css-loader": "^5.2.6", "css-loader": "^5.2.6",
"deepdash": "^5.3.5", "deepdash": "^5.3.5",
"dompurify": "^2.0.17", "dompurify": "^2.0.17",
"electron": "^9.4.4", "electron": "^12.0.17",
"electron-builder": "^22.10.5", "electron-builder": "^22.10.5",
"electron-notarize": "^0.3.0", "electron-notarize": "^0.3.0",
"esbuild": "^0.12.12", "esbuild": "^0.12.12",
@ -352,8 +356,9 @@
"node-loader": "^1.0.3", "node-loader": "^1.0.3",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"nodemon": "^2.0.12", "nodemon": "^2.0.12",
"playwright": "^1.14.0",
"postcss": "^8.3.6", "postcss": "^8.3.6",
"postcss-loader": "4.0.3", "postcss-loader": "4.3.0",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"progress-bar-webpack-plugin": "^2.1.0", "progress-bar-webpack-plugin": "^2.1.0",
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",
@ -367,12 +372,11 @@
"react-window": "^1.8.5", "react-window": "^1.8.5",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"sharp": "^0.29.0", "sharp": "^0.29.0",
"spectron": "11.0.0",
"style-loader": "^2.0.0", "style-loader": "^2.0.0",
"tailwindcss": "^2.2.4", "tailwindcss": "^2.2.4",
"ts-jest": "26.5.6", "ts-jest": "26.5.6",
"ts-loader": "^7.0.5", "ts-loader": "^7.0.5",
"ts-node": "^8.10.2", "ts-node": "^10.1.0",
"type-fest": "^1.0.2", "type-fest": "^1.0.2",
"typed-emitter": "^1.3.1", "typed-emitter": "^1.3.1",
"typedoc": "0.21.0-beta.2", "typedoc": "0.21.0-beta.2",

View File

@ -37,7 +37,6 @@ import { Console } from "console";
import { SemVer } from "semver"; import { SemVer } from "semver";
import electron from "electron"; import electron from "electron";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";
import { beforeEachWrapped } from "../../../integration/helpers/utils";
import { ThemeStore } from "../../renderer/theme.store"; import { ThemeStore } from "../../renderer/theme.store";
import type { ClusterStoreModel } from "../cluster-store"; import type { ClusterStoreModel } from "../cluster-store";
@ -45,7 +44,7 @@ console = new Console(stdout, stderr);
describe("user store tests", () => { describe("user store tests", () => {
describe("for an empty config", () => { describe("for an empty config", () => {
beforeEachWrapped(() => { beforeEach(() => {
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } }); mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve()); (UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
@ -94,7 +93,7 @@ describe("user store tests", () => {
}); });
describe("migrations", () => { describe("migrations", () => {
beforeEachWrapped(() => { beforeEach(() => {
mockFs({ mockFs({
"tmp": { "tmp": {
"config.json": JSON.stringify({ "config.json": JSON.stringify({

View File

@ -22,7 +22,7 @@
import path from "path"; import path from "path";
import Config from "conf"; import Config from "conf";
import type { Options as ConfOptions } from "conf/dist/source/types"; import type { Options as ConfOptions } from "conf/dist/source/types";
import { app, ipcMain, ipcRenderer, remote } from "electron"; import { ipcMain, ipcRenderer } from "electron";
import { IReactionOptions, makeObservable, reaction, runInAction } from "mobx"; import { IReactionOptions, makeObservable, reaction, runInAction } from "mobx";
import { getAppVersion, Singleton, toJS, Disposer } from "./utils"; import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
import logger from "../main/logger"; import logger from "../main/logger";
@ -30,6 +30,7 @@ import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import { isTestEnv } from "./vars"; import { isTestEnv } from "./vars";
import { kebabCase } from "lodash"; import { kebabCase } from "lodash";
import { getPath } from "./utils/getPath";
export interface BaseStoreParams<T> extends ConfOptions<T> { export interface BaseStoreParams<T> extends ConfOptions<T> {
syncOptions?: IReactionOptions; syncOptions?: IReactionOptions;
@ -88,7 +89,7 @@ export abstract class BaseStore<T> extends Singleton {
} }
protected cwd() { protected cwd() {
return (app || remote.app).getPath("userData"); return getPath("userData");
} }
protected async saveToFile(model: T) { protected async saveToFile(model: T) {

View File

@ -19,6 +19,8 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
export const dialogShowOpenDialogHandler = "dialog:show-open-dialog";
export * from "./ipc"; export * from "./ipc";
export * from "./invalid-kubeconfig"; export * from "./invalid-kubeconfig";
export * from "./update-available.ipc"; export * from "./update-available.ipc";

View File

@ -23,12 +23,14 @@
// https://www.electronjs.org/docs/api/ipc-main // https://www.electronjs.org/docs/api/ipc-main
// https://www.electronjs.org/docs/api/ipc-renderer // https://www.electronjs.org/docs/api/ipc-renderer
import { ipcMain, ipcRenderer, remote, webContents } from "electron"; import { ipcMain, ipcRenderer, webContents } from "electron";
import { toJS } from "../utils/toJS"; import { toJS } from "../utils/toJS";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames"; import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
import type { Disposer } from "../utils"; import type { Disposer } from "../utils";
const remote = ipcMain ? null : require("@electron/remote");
const subFramesChannel = "ipc:get-sub-frames"; const subFramesChannel = "ipc:get-sub-frames";
export async function requestMain(channel: string, ...args: any[]) { export async function requestMain(channel: string, ...args: any[]) {

View File

@ -26,8 +26,7 @@ import type { RequestInit } from "node-fetch";
import { autoBind, bifurcateArray } from "../../utils"; import { autoBind, bifurcateArray } from "../../utils";
import Joi from "joi"; import Joi from "joi";
export type RepoHelmChartList = Record<string, HelmChart[]>; export type RepoHelmChartList = Record<string, RawHelmChart[]>;
export type HelmChartList = Record<string, RepoHelmChartList>;
export interface IHelmChartDetails { export interface IHelmChartDetails {
readme: string; readme: string;
@ -43,7 +42,7 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: {
* Get a list of all helm charts from all saved helm repos * Get a list of all helm charts from all saved helm repos
*/ */
export async function listCharts(): Promise<HelmChart[]> { export async function listCharts(): Promise<HelmChart[]> {
const data = await apiBase.get<HelmChartList>(endpoint()); const data = await apiBase.get<Record<string, RepoHelmChartList>>(endpoint());
return Object return Object
.values(data) .values(data)
@ -311,11 +310,9 @@ export class HelmChart {
} }
static create(data: RawHelmChart, { onError = "throw" }: HelmChartCreateOpts = {}): HelmChart | undefined { static create(data: RawHelmChart, { onError = "throw" }: HelmChartCreateOpts = {}): HelmChart | undefined {
const result = helmChartValidator.validate(data, { const { value, error } = helmChartValidator.validate(data, {
abortEarly: false, abortEarly: false,
}); });
let { error } = result;
const { value } = result;
if (!error) { if (!error) {
return new HelmChart(value); return new HelmChart(value);
@ -331,13 +328,13 @@ export class HelmChart {
return new HelmChart(value); return new HelmChart(value);
} }
error = new Joi.ValidationError(actualErrors.map(er => er.message).join(". "), actualErrors, error._original); const validationError = new Joi.ValidationError(actualErrors.map(er => er.message).join(". "), actualErrors, error._original);
if (onError === "throw") { if (onError === "throw") {
throw error; throw validationError;
} }
console.warn("[HELM-CHART]: failed to validate data", data, error); console.warn("[HELM-CHART]: failed to validate data", data, validationError);
return undefined; return undefined;
} }

View File

@ -19,19 +19,22 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { app, ipcMain, remote } from "electron"; import { ipcMain } from "electron";
import winston, { format } from "winston"; import winston, { format } from "winston";
import type Transport from "winston-transport"; import type Transport from "winston-transport";
import { consoleFormat } from "winston-console-format"; import { consoleFormat } from "winston-console-format";
import { isDebugging, isTestEnv } from "./vars"; import { isDebugging, isTestEnv } from "./vars";
import BrowserConsole from "winston-transport-browserconsole"; import BrowserConsole from "winston-transport-browserconsole";
import { SentryTransport } from "./logger-transports"; import { SentryTransport } from "./logger-transports";
import { getPath } from "./utils/getPath";
const logLevel = process.env.LOG_LEVEL const logLevel = process.env.LOG_LEVEL
? process.env.LOG_LEVEL ? process.env.LOG_LEVEL
: isDebugging : isDebugging
? "debug" ? "debug"
: "info"; : isTestEnv
? "error"
: "info";
const transports: Transport[] = [ const transports: Transport[] = [
new SentryTransport("error") new SentryTransport("error")
@ -69,7 +72,7 @@ if (!isTestEnv) {
handleExceptions: false, handleExceptions: false,
level: logLevel, level: logLevel,
filename: "lens.log", filename: "lens.log",
dirname: (app ?? remote?.app)?.getPath("logs"), dirname: getPath("logs"),
maxsize: 16 * 1024, maxsize: 16 * 1024,
maxFiles: 16, maxFiles: 16,
tailable: true, tailable: true,

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { app, remote } from "electron"; import { app } from "electron";
import semver from "semver"; import semver from "semver";
import { action, computed, observable, reaction, makeObservable } from "mobx"; import { action, computed, observable, reaction, makeObservable } from "mobx";
import { BaseStore } from "../base-store"; import { BaseStore } from "../base-store";
@ -32,7 +32,8 @@ import { fileNameMigration } from "../../migrations/user-store";
import { ObservableToggleSet, toJS } from "../../renderer/utils"; import { ObservableToggleSet, toJS } from "../../renderer/utils";
import { DESCRIPTORS, KubeconfigSyncValue, UserPreferencesModel, EditorConfiguration } from "./preferences-helpers"; import { DESCRIPTORS, KubeconfigSyncValue, UserPreferencesModel, EditorConfiguration } from "./preferences-helpers";
import logger from "../../main/logger"; import logger from "../../main/logger";
import type {monaco} from "react-monaco-editor"; import type { monaco } from "react-monaco-editor";
import { getPath } from "../utils/getPath";
export interface UserStoreModel { export interface UserStoreModel {
lastSeenAppVersion: string; lastSeenAppVersion: string;
@ -69,7 +70,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
@observable shell?: string; @observable shell?: string;
@observable downloadBinariesPath?: string; @observable downloadBinariesPath?: string;
@observable kubectlBinariesPath?: string; @observable kubectlBinariesPath?: string;
/** /**
* Download kubectl binaries matching cluster version * Download kubectl binaries matching cluster version
*/ */
@ -86,7 +87,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
* Monaco editor configs * Monaco editor configs
*/ */
@observable editorConfiguration:EditorConfiguration = {tabSize: null, miniMap: null, lineNumbers: null}; @observable editorConfiguration:EditorConfiguration = {tabSize: null, miniMap: null, lineNumbers: null};
/** /**
* The set of file/folder paths to be synced * The set of file/folder paths to be synced
*/ */
@ -115,7 +116,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
}); });
}, { }, {
fireImmediately: true, fireImmediately: true,
}); });
} }
// Returns monaco editor options for selected editor type (the place, where a particular instance of the editor is mounted) // Returns monaco editor options for selected editor type (the place, where a particular instance of the editor is mounted)
@ -244,5 +245,5 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
* @returns string * @returns string
*/ */
export function getDefaultKubectlDownloadPath(): string { export function getDefaultKubectlDownloadPath(): string {
return path.join((app || remote.app).getPath("userData"), "binaries"); return path.join(getPath("userData"), "binaries");
} }

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { app, ipcMain } from "electron";
const remote = ipcMain ? null : require("@electron/remote");
/**
* calls getPath either on app or on the remote's app
*
* @deprecated Use a different method for accessing the getPath function
*/
export function getPath(name: Parameters<typeof app["getPath"]>[0]): string {
if (app) {
return app.getPath(name);
}
return remote.app.getPath(name);
}

View File

@ -40,6 +40,7 @@ export * from "./downloadFile";
export * from "./formatDuration"; export * from "./formatDuration";
export * from "./escapeRegExp"; export * from "./escapeRegExp";
export * from "./extended-map"; export * from "./extended-map";
export * from "./getPath";
export * from "./getRandId"; export * from "./getRandId";
export * from "./hash-set"; export * from "./hash-set";
export * from "./local-kubeconfig"; export * from "./local-kubeconfig";

View File

@ -19,13 +19,13 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { app, remote } from "electron";
import path from "path"; import path from "path";
import * as uuid from "uuid"; import * as uuid from "uuid";
import type { ClusterId } from "../cluster-types"; import type { ClusterId } from "../cluster-types";
import { getPath } from "./getPath";
export function storedKubeConfigFolder(): string { export function storedKubeConfigFolder(): string {
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs"); return path.resolve(getPath("userData"), "kubeconfigs");
} }
export function getCustomKubeConfigPath(clusterId: ClusterId = uuid.v4()): string { export function getCustomKubeConfigPath(clusterId: ClusterId = uuid.v4()): string {

View File

@ -19,7 +19,10 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import semver, { SemVer } from "semver"; import semver, { coerce, SemVer } from "semver";
import * as iter from "./iter";
import type { RawHelmChart } from "../k8s-api/endpoints/helm-charts.api";
import logger from "../logger";
export function sortCompare<T>(left: T, right: T): -1 | 0 | 1 { export function sortCompare<T>(left: T, right: T): -1 | 0 | 1 {
if (left < right) { if (left < right) {
@ -53,3 +56,32 @@ export function sortCompareChartVersions(left: ChartVersion, right: ChartVersion
return sortCompare(left.version, right.version); return sortCompare(left.version, right.version);
} }
export function sortCharts(charts: RawHelmChart[]) {
interface ExtendedHelmChart extends RawHelmChart {
__version: SemVer;
}
const chartsWithVersion = Array.from(
iter.map(
charts,
(chart => {
const __version = coerce(chart.version, { includePrerelease: true, loose: true });
if (!__version) {
logger.warn(`[HELM-SERVICE]: Version from helm chart is not loosely coercable to semver.`, { name: chart.name, version: chart.version, repo: chart.repo });
}
(chart as ExtendedHelmChart).__version = __version;
return chart as ExtendedHelmChart;
})
),
);
return chartsWithVersion
.sort(sortCompareChartVersions)
.map(chart => (delete chart.__version, chart));
}

View File

@ -35,6 +35,9 @@ export const isTestEnv = !!process.env.JEST_WORKER_ID;
export const isDevelopment = !isTestEnv && !isProduction; export const isDevelopment = !isTestEnv && !isProduction;
export const isPublishConfigured = Object.keys(packageInfo.build).includes("publish"); export const isPublishConfigured = Object.keys(packageInfo.build).includes("publish");
export const integrationTestingArg = "--integration-testing";
export const isIntegrationTesting = process.argv.includes(integrationTestingArg);
export const productName = packageInfo.productName; export const productName = packageInfo.productName;
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`; export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
export const publicPath = "/build/" as string; export const publicPath = "/build/" as string;

View File

@ -19,14 +19,14 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { app, ipcRenderer, remote } from "electron"; import { ipcRenderer } from "electron";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx"; import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
import path from "path"; import path from "path";
import { ClusterStore } from "../common/cluster-store"; import { ClusterStore } from "../common/cluster-store";
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../common/ipc"; import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../common/ipc";
import { Disposer, getHostedClusterId, Singleton, toJS } from "../common/utils"; import { Disposer, getHostedClusterId, Singleton, toJS, getPath } from "../common/utils";
import logger from "../main/logger"; import logger from "../main/logger";
import type { InstalledExtension } from "./extension-discovery"; import type { InstalledExtension } from "./extension-discovery";
import { ExtensionsStore } from "./extensions-store"; import { ExtensionsStore } from "./extensions-store";
@ -36,7 +36,7 @@ import type { LensRendererExtension } from "./lens-renderer-extension";
import * as registries from "./registries"; import * as registries from "./registries";
export function extensionPackagesRoot() { export function extensionPackagesRoot() {
return path.join((app || remote.app).getPath("userData")); return path.join(getPath("userData"));
} }
const logModule = "[EXTENSIONS-LOADER]"; const logModule = "[EXTENSIONS-LOADER]";

View File

@ -33,7 +33,8 @@ import { VersionDetector } from "./cluster-detectors/version-detector";
import { DetectorRegistry } from "./cluster-detectors/detector-registry"; import { DetectorRegistry } from "./cluster-detectors/detector-registry";
import plimit from "p-limit"; import plimit from "p-limit";
import { toJS } from "../common/utils"; import { toJS } from "../common/utils";
import { initialNodeShellImage, ClusterState, ClusterMetadataKey, ClusterRefreshOptions, ClusterStatus, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-types"; import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types";
/** /**
* Cluster * Cluster

View File

@ -26,14 +26,9 @@ import { ClusterManager } from "./cluster-manager";
import logger from "./logger"; import logger from "./logger";
export function exitApp() { export function exitApp() {
console.log("before windowManager");
const windowManager = WindowManager.getInstance(false); const windowManager = WindowManager.getInstance(false);
console.log("before clusterManager");
const clusterManager = ClusterManager.getInstance(false); const clusterManager = ClusterManager.getInstance(false);
console.log("after clusterManager");
appEventBus.emit({ name: "service", action: "close" }); appEventBus.emit({ name: "service", action: "close" });
windowManager?.hide(); windowManager?.hide();
clusterManager?.stop(); clusterManager?.stop();

View File

@ -21,13 +21,13 @@
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { SHA256 } from "crypto-js"; import { SHA256 } from "crypto-js";
import { app, remote } from "electron";
import fse from "fs-extra"; import fse from "fs-extra";
import { action, makeObservable, observable } from "mobx"; import { action, makeObservable, observable } from "mobx";
import path from "path"; import path from "path";
import { BaseStore } from "../common/base-store"; import { BaseStore } from "../common/base-store";
import type { LensExtensionId } from "../extensions/lens-extension"; import type { LensExtensionId } from "../extensions/lens-extension";
import { toJS } from "../common/utils"; import { toJS } from "../common/utils";
import { getPath } from "../common/utils/getPath";
interface FSProvisionModel { interface FSProvisionModel {
extensions: Record<string, string>; // extension names to paths extensions: Record<string, string>; // extension names to paths
@ -55,7 +55,7 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
if (!this.registeredExtensions.has(extensionName)) { if (!this.registeredExtensions.has(extensionName)) {
const salt = randomBytes(32).toString("hex"); const salt = randomBytes(32).toString("hex");
const hashedName = SHA256(`${extensionName}/${salt}`).toString(); const hashedName = SHA256(`${extensionName}/${salt}`).toString();
const dirPath = path.resolve((app || remote.app).getPath("userData"), "extension_data", hashedName); const dirPath = path.resolve(getPath("userData"), "extension_data", hashedName);
this.registeredExtensions.set(extensionName, dirPath); this.registeredExtensions.set(extensionName, dirPath);
} }

View File

@ -19,141 +19,151 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { HelmRepo, HelmRepoManager } from "../helm-repo-manager"; import { sortCharts } from "../../../common/utils";
import type { HelmRepo } from "../helm-repo-manager";
const charts = new Map([
["stable", {
"invalid-semver": sortCharts([
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "v4.3.0",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver but more",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "v4.4.0",
repo: "stable",
digest: "test",
created: "now",
},
]),
"apm-server": sortCharts([
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.7",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.6",
repo: "stable",
digest: "test",
created: "now",
}
]),
"redis": sortCharts([
{
apiVersion: "3.0.0",
name: "apm-server",
version: "1.0.0",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "0.0.9",
repo: "stable",
digest: "test",
created: "now",
}
]),
}],
["experiment", {
"fairwind": sortCharts([
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.1",
repo: "experiment",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.2",
repo: "experiment",
digest: "test",
deprecated: true,
created: "now",
}
]),
}],
["bitnami", {
"hotdog": sortCharts([
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.1",
repo: "bitnami",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.2",
repo: "bitnami",
digest: "test",
created: "now",
}
]),
"pretzel": sortCharts([
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0",
repo: "bitnami",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0.1",
repo: "bitnami",
digest: "test",
created: "now",
}
]),
}],
]);
export class HelmChartManager { export class HelmChartManager {
cache: any = {}; constructor(private repo: HelmRepo){ }
private repo: HelmRepo;
constructor(repo: HelmRepo){ static forRepo(repo: HelmRepo) {
this.cache = HelmRepoManager.cache; return new this(repo);
this.repo = repo;
} }
public async charts(): Promise<any> { public async charts(): Promise<any> {
switch (this.repo.name) { return charts.get(this.repo.name) ?? {};
case "stable":
return Promise.resolve({
"invalid-semver": [
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "v4.3.0",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver but more",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "v4.4.0",
repo: "stable",
digest: "test"
},
],
"apm-server": [
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.7",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.6",
repo: "stable",
digest: "test"
}
],
"redis": [
{
apiVersion: "3.0.0",
name: "apm-server",
version: "1.0.0",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "0.0.9",
repo: "stable",
digest: "test"
}
]
});
case "experiment":
return Promise.resolve({
"fairwind": [
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.1",
repo: "experiment",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.2",
repo: "experiment",
digest: "test",
deprecated: true
}
]
});
case "bitnami":
return Promise.resolve({
"hotdog": [
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.1",
repo: "bitnami",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.2",
repo: "bitnami",
digest: "test",
}
],
"pretzel": [
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0",
repo: "bitnami",
digest: "test",
},
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0.1",
repo: "bitnami",
digest: "test"
}
]
});
default:
return Promise.resolve({});
}
} }
} }

View File

@ -31,7 +31,7 @@ describe("Helm Service tests", () => {
jest.resetAllMocks(); jest.resetAllMocks();
}); });
it("list charts without deprecated ones", async () => { it("list charts with deprecated entries", async () => {
mockHelmRepoManager.mockReturnValue({ mockHelmRepoManager.mockReturnValue({
init: jest.fn(), init: jest.fn(),
repositories: jest.fn().mockImplementation(async () => { repositories: jest.fn().mockImplementation(async () => {
@ -52,14 +52,16 @@ describe("Helm Service tests", () => {
name: "apm-server", name: "apm-server",
version: "2.1.7", version: "2.1.7",
repo: "stable", repo: "stable",
digest: "test" digest: "test",
created: "now",
}, },
{ {
apiVersion: "3.0.0", apiVersion: "3.0.0",
name: "apm-server", name: "apm-server",
version: "2.1.6", version: "2.1.6",
repo: "stable", repo: "stable",
digest: "test" digest: "test",
created: "now",
} }
], ],
"invalid-semver": [ "invalid-semver": [
@ -68,28 +70,32 @@ describe("Helm Service tests", () => {
name: "weird-versioning", name: "weird-versioning",
version: "v4.4.0", version: "v4.4.0",
repo: "stable", repo: "stable",
digest: "test" digest: "test",
created: "now",
}, },
{ {
apiVersion: "3.0.0", apiVersion: "3.0.0",
name: "weird-versioning", name: "weird-versioning",
version: "v4.3.0", version: "v4.3.0",
repo: "stable", repo: "stable",
digest: "test" digest: "test",
created: "now",
}, },
{ {
apiVersion: "3.0.0", apiVersion: "3.0.0",
name: "weird-versioning", name: "weird-versioning",
version: "I am not semver", version: "I am not semver",
repo: "stable", repo: "stable",
digest: "test" digest: "test",
created: "now",
}, },
{ {
apiVersion: "3.0.0", apiVersion: "3.0.0",
name: "weird-versioning", name: "weird-versioning",
version: "I am not semver but more", version: "I am not semver but more",
repo: "stable", repo: "stable",
digest: "test" digest: "test",
created: "now",
}, },
], ],
"redis": [ "redis": [
@ -98,18 +104,40 @@ describe("Helm Service tests", () => {
name: "apm-server", name: "apm-server",
version: "1.0.0", version: "1.0.0",
repo: "stable", repo: "stable",
digest: "test" digest: "test",
created: "now",
}, },
{ {
apiVersion: "3.0.0", apiVersion: "3.0.0",
name: "apm-server", name: "apm-server",
version: "0.0.9", version: "0.0.9",
repo: "stable", repo: "stable",
digest: "test" digest: "test",
created: "now",
} }
] ]
}, },
experiment: {} experiment: {
"fairwind": [
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.2",
repo: "experiment",
digest: "test",
deprecated: true,
created: "now",
},
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.1",
repo: "experiment",
digest: "test",
created: "now",
},
]
}
}); });
}); });
@ -134,13 +162,15 @@ describe("Helm Service tests", () => {
version: "1.0.2", version: "1.0.2",
repo: "bitnami", repo: "bitnami",
digest: "test", digest: "test",
created: "now",
}, },
{ {
apiVersion: "3.0.0", apiVersion: "3.0.0",
name: "hotdog", name: "hotdog",
version: "1.0.1", version: "1.0.1",
repo: "bitnami", repo: "bitnami",
digest: "test" digest: "test",
created: "now",
}, },
], ],
"pretzel": [ "pretzel": [
@ -150,13 +180,15 @@ describe("Helm Service tests", () => {
version: "1.0.1", version: "1.0.1",
repo: "bitnami", repo: "bitnami",
digest: "test", digest: "test",
created: "now",
}, },
{ {
apiVersion: "3.0.0", apiVersion: "3.0.0",
name: "pretzel", name: "pretzel",
version: "1.0", version: "1.0",
repo: "bitnami", repo: "bitnami",
digest: "test" digest: "test",
created: "now",
} }
] ]
} }

View File

@ -20,27 +20,25 @@
*/ */
import fs from "fs"; import fs from "fs";
import v8 from "v8";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"; import type { HelmRepo } from "./helm-repo-manager";
import logger from "../logger"; import logger from "../logger";
import { promiseExec } from "../promise-exec"; import { promiseExec } from "../promise-exec";
import { helmCli } from "./helm-cli"; import { helmCli } from "./helm-cli";
import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api"; import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api";
import { sortCharts } from "../../common/utils";
type CachedYaml = {
entries: RepoHelmChartList
};
export class HelmChartManager { export class HelmChartManager {
protected cache: any = {}; static #cache = new Map<string, Buffer>();
protected repo: HelmRepo;
constructor(repo: HelmRepo){ private constructor(protected repo: HelmRepo) {}
this.cache = HelmRepoManager.cache;
this.repo = repo; static forRepo(repo: HelmRepo) {
return new this(repo);
} }
public async chart(name: string) { public async chartVersions(name: string) {
const charts = await this.charts(); const charts = await this.charts();
return charts[name]; return charts[name];
@ -48,9 +46,7 @@ export class HelmChartManager {
public async charts(): Promise<RepoHelmChartList> { public async charts(): Promise<RepoHelmChartList> {
try { try {
const cachedYaml = await this.cachedYaml(); return await this.cachedYaml();
return cachedYaml["entries"];
} catch(error) { } catch(error) {
logger.error("HELM-CHART-MANAGER]: failed to list charts", { error }); logger.error("HELM-CHART-MANAGER]: failed to list charts", { error });
@ -58,48 +54,61 @@ export class HelmChartManager {
} }
} }
public async getReadme(name: string, version = "") { private async executeCommand(action: string, name: string, version?: string) {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const cmd = [`"${helm}" ${action} ${this.repo.name}/${name}`];
if(version && version != "") { if (version) {
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);}); cmd.push("--version", version);
}
return stdout;
} else { try {
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(cmd.join(" "));
return stdout; return stdout;
} catch (error) {
throw error.stderr || error;
} }
} }
public async getValues(name: string, version = "") { public async getReadme(name: string, version?: string) {
const helm = await helmCli.binaryPath(); return this.executeCommand("show readme", name, version);
if(version && version != "") {
const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
return stdout;
} else {
const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
return stdout;
}
} }
protected async cachedYaml(): Promise<CachedYaml> { public async getValues(name: string, version?: string) {
if (!(this.repo.name in this.cache)) { return this.executeCommand("show values", name, version);
}
protected async cachedYaml(): Promise<RepoHelmChartList> {
if (!HelmChartManager.#cache.has(this.repo.name)) {
const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8"); const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8");
const data = yaml.safeLoad(cacheFile); const { entries } = yaml.safeLoad(cacheFile) as { entries: RepoHelmChartList };
for(const key in data["entries"]) { /**
data["entries"][key].forEach((version: any) => { * Do some initial preprocessing on the data, so as to avoid needing to do it later
version["repo"] = this.repo.name; * 1. Set the repo name
version["created"] = Date.parse(version.created).toString(); * 2. Normalize the created date
}); * 3. Filter out deprecated items
} */
this.cache[this.repo.name] = Buffer.from(JSON.stringify(data));
const normalized = Object.fromEntries(
Object.entries(entries)
.map(([name, charts]) => [
name,
sortCharts(
charts.map(chart => ({
...chart,
created: Date.parse(chart.created).toString(),
repo: this.repo.name,
})),
),
] as const)
.filter(([, charts]) => !charts.every(chart => chart.deprecated))
);
HelmChartManager.#cache.set(this.repo.name, v8.serialize(normalized));
} }
return JSON.parse(this.cache[this.repo.name].toString()); return v8.deserialize(HelmChartManager.#cache.get(this.repo.name));
} }
} }

View File

@ -50,8 +50,6 @@ export interface HelmRepo {
} }
export class HelmRepoManager extends Singleton { export class HelmRepoManager extends Singleton {
static cache = {}; // todo: remove implicit updates in helm-chart-manager.ts
protected repos: HelmRepo[]; protected repos: HelmRepo[];
protected helmEnv: HelmEnv; protected helmEnv: HelmEnv;
protected initialized: boolean; protected initialized: boolean;
@ -97,6 +95,12 @@ export class HelmRepoManager extends Singleton {
return env; return env;
} }
public async repo(name: string): Promise<HelmRepo> {
const repos = await this.repositories();
return repos.find(repo => repo.name === name);
}
public async repositories(): Promise<HelmRepo[]> { public async repositories(): Promise<HelmRepo[]> {
try { try {
if (!this.initialized) { if (!this.initialized) {

View File

@ -19,14 +19,11 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import semver, { SemVer } from "semver";
import type { Cluster } from "../cluster"; import type { Cluster } from "../cluster";
import logger from "../logger"; import logger from "../logger";
import { HelmRepoManager } from "./helm-repo-manager"; import { HelmRepoManager } from "./helm-repo-manager";
import { HelmChartManager } from "./helm-chart-manager"; import { HelmChartManager } from "./helm-chart-manager";
import type { HelmChart, HelmChartList, RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api";
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager"; import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
import { iter, sortCompareChartVersions } from "../../common/utils";
interface GetReleaseValuesArgs { interface GetReleaseValuesArgs {
cluster: Cluster; cluster: Cluster;
@ -42,43 +39,27 @@ class HelmService {
} }
public async listCharts() { public async listCharts() {
const charts: HelmChartList = {};
const repositories = await HelmRepoManager.getInstance().repositories(); const repositories = await HelmRepoManager.getInstance().repositories();
for (const repo of repositories) { return Object.fromEntries(
charts[repo.name] = {}; await Promise.all(repositories.map(async repo => [repo.name, await HelmChartManager.forRepo(repo).charts()]))
const manager = new HelmChartManager(repo); );
const sortedCharts = this.sortChartsByVersion(await manager.charts());
const enabledCharts = this.excludeDeprecatedChartGroups(sortedCharts);
charts[repo.name] = enabledCharts;
}
return charts;
} }
public async getChart(repoName: string, chartName: string, version = "") { public async getChart(repoName: string, chartName: string, version = "") {
const result = { const repo = await HelmRepoManager.getInstance().repo(repoName);
readme: "", const chartManager = HelmChartManager.forRepo(repo);
versions: {}
return {
readme: await chartManager.getReadme(chartName, version),
versions: await chartManager.chartVersions(chartName),
}; };
const repos = await HelmRepoManager.getInstance().repositories();
const repo = repos.find(repo => repo.name === repoName);
const chartManager = new HelmChartManager(repo);
const chart = await chartManager.chart(chartName);
result.readme = await chartManager.getReadme(chartName, version);
result.versions = chart;
return result;
} }
public async getChartValues(repoName: string, chartName: string, version = "") { public async getChartValues(repoName: string, chartName: string, version = "") {
const repos = await HelmRepoManager.getInstance().repositories(); const repo = await HelmRepoManager.getInstance().repo(repoName);
const repo = repos.find(repo => repo.name === repoName);
const chartManager = new HelmChartManager(repo);
return chartManager.getValues(chartName, version); return HelmChartManager.forRepo(repo).getValues(chartName, version);
} }
public async listReleases(cluster: Cluster, namespace: string = null) { public async listReleases(cluster: Cluster, namespace: string = null) {
@ -131,58 +112,6 @@ class HelmService {
return { message: output }; return { message: output };
} }
private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) {
return Object.fromEntries(
iter.filterMap(
Object.entries(chartGroups),
([name, charts]) => {
for (const chart of charts) {
if (chart.deprecated) {
// ignore chart group if any chart is deprecated
return undefined;
}
}
return [name, charts];
}
)
);
}
private sortCharts(charts: HelmChart[]) {
interface ExtendedHelmChart extends HelmChart {
__version: SemVer;
}
const chartsWithVersion = Array.from(
iter.map(
charts,
(chart => {
const __version = semver.coerce(chart.version, { includePrerelease: true, loose: true });
if (!__version) {
logger.error(`[HELM-SERVICE]: Version from helm chart is not loosely coercable to semver.`, { name: chart.name, version: chart.version, repo: chart.repo });
}
(chart as ExtendedHelmChart).__version = __version;
return chart as ExtendedHelmChart;
})
),
);
return chartsWithVersion
.sort(sortCompareChartVersions)
.map(chart => (delete chart.__version, chart as HelmChart));
}
private sortChartsByVersion(chartGroups: RepoHelmChartList) {
return Object.fromEntries(
Object.entries(chartGroups)
.map(([name, charts]) => [name, this.sortCharts(charts)])
);
}
} }
export const helmService = new HelmService(); export const helmService = new HelmService();

View File

@ -22,12 +22,12 @@
// Main process // Main process
import "../common/system-ca"; import "../common/system-ca";
import { initialize as initializeRemote } from "@electron/remote/main";
import * as Mobx from "mobx"; import * as Mobx from "mobx";
import * as LensExtensionsCommonApi from "../extensions/common-api"; import * as LensExtensionsCommonApi from "../extensions/common-api";
import * as LensExtensionsMainApi from "../extensions/main-api"; import * as LensExtensionsMainApi from "../extensions/main-api";
import { app, autoUpdater, dialog, powerMonitor } from "electron"; import { app, autoUpdater, dialog, powerMonitor } from "electron";
import { appName, isMac, productName } from "../common/vars"; import { appName, isIntegrationTesting, isMac, productName } from "../common/vars";
import path from "path";
import { LensProxy } from "./lens-proxy"; import { LensProxy } from "./lens-proxy";
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager"; import { ClusterManager } from "./cluster-manager";
@ -63,13 +63,13 @@ import { ensureDir } from "fs-extra";
import { Router } from "./router"; import { Router } from "./router";
import { initMenu } from "./menu"; import { initMenu } from "./menu";
import { initTray } from "./tray"; import { initTray } from "./tray";
import * as path from "path";
import { kubeApiRequest, shellApiRequest } from "./proxy-functions"; import { kubeApiRequest, shellApiRequest } from "./proxy-functions";
const onCloseCleanup = disposer();
const onQuitCleanup = disposer();
SentryInit(); SentryInit();
const workingDir = path.join(app.getPath("appData"), appName);
const cleanup = disposer();
app.setName(appName); app.setName(appName);
logger.info(`📟 Setting ${productName} as protocol client for lens://`); logger.info(`📟 Setting ${productName} as protocol client for lens://`);
@ -80,14 +80,16 @@ if (app.setAsDefaultProtocolClient("lens")) {
logger.info("📟 Protocol client register failed ❗"); logger.info("📟 Protocol client register failed ❗");
} }
if (!process.env.CICD) { if (process.env.CICD) {
app.setPath("userData", workingDir); app.setPath("appData", process.env.CICD);
app.setPath("userData", path.join(process.env.CICD, appName));
} }
if (process.env.LENS_DISABLE_GPU) { if (process.env.LENS_DISABLE_GPU) {
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
} }
initializeRemote();
configurePackages(); configurePackages();
mangleProxyEnv(); mangleProxyEnv();
initializers.initIpcMainHandlers(); initializers.initIpcMainHandlers();
@ -121,7 +123,7 @@ app.on("second-instance", (event, argv) => {
}); });
app.on("ready", async () => { app.on("ready", async () => {
logger.info(`🚀 Starting ${productName} from "${workingDir}"`); logger.info(`🚀 Starting ${productName} from "${app.getPath("exe")}"`);
logger.info("🐚 Syncing shell environment"); logger.info("🐚 Syncing shell environment");
await shellSync(); await shellSync();
@ -209,7 +211,7 @@ app.on("ready", async () => {
logger.info("🖥️ Starting WindowManager"); logger.info("🖥️ Starting WindowManager");
const windowManager = WindowManager.createInstance(); const windowManager = WindowManager.createInstance();
cleanup.push( onQuitCleanup.push(
initMenu(windowManager), initMenu(windowManager),
initTray(windowManager), initTray(windowManager),
); );
@ -221,7 +223,7 @@ app.on("ready", async () => {
} }
ipcMainOn(IpcRendererNavigationEvents.LOADED, async () => { ipcMainOn(IpcRendererNavigationEvents.LOADED, async () => {
cleanup.push(pushCatalogToRenderer(catalogEntityRegistry)); onCloseCleanup.push(pushCatalogToRenderer(catalogEntityRegistry));
await ensureDir(storedKubeConfigFolder()); await ensureDir(storedKubeConfigFolder());
KubeconfigSyncManager.getInstance().startSync(); KubeconfigSyncManager.getInstance().startSync();
startUpdateChecking(); startUpdateChecking();
@ -269,7 +271,7 @@ app.on("activate", (event, hasVisibleWindows) => {
/** /**
* This variable should is used so that `autoUpdater.installAndQuit()` works * This variable should is used so that `autoUpdater.installAndQuit()` works
*/ */
let blockQuit = true; let blockQuit = !isIntegrationTesting;
autoUpdater.on("before-quit-for-update", () => blockQuit = false); autoUpdater.on("before-quit-for-update", () => blockQuit = false);
@ -282,7 +284,7 @@ app.on("will-quit", (event) => {
appEventBus.emit({ name: "app", action: "close" }); appEventBus.emit({ name: "app", action: "close" });
ClusterManager.getInstance(false)?.stop(); // close cluster connections ClusterManager.getInstance(false)?.stop(); // close cluster connections
KubeconfigSyncManager.getInstance(false)?.stopSync(); KubeconfigSyncManager.getInstance(false)?.stopSync();
cleanup(); onCloseCleanup();
if (lprm) { if (lprm) {
// This is set to false here so that LPRM can wait to send future lens:// // This is set to false here so that LPRM can wait to send future lens://
@ -298,7 +300,8 @@ app.on("will-quit", (event) => {
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
} }
LensProtocolRouterMain.getInstance(false)?.cleanup(); lprm?.cleanup();
onQuitCleanup();
}); });
app.on("open-url", (event, rawUrl) => { app.on("open-url", (event, rawUrl) => {

View File

@ -19,20 +19,21 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { IpcMainInvokeEvent } from "electron"; import { BrowserWindow, dialog, IpcMainInvokeEvent } from "electron";
import { KubernetesCluster } from "../../common/catalog-entities"; import { KubernetesCluster } from "../../common/catalog-entities";
import { clusterFrameMap } from "../../common/cluster-frames"; import { clusterFrameMap } from "../../common/cluster-frames";
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc"; import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc";
import { ClusterStore } from "../../common/cluster-store"; import { ClusterStore } from "../../common/cluster-store";
import type { ClusterId } from "../../common/cluster-types"; import type { ClusterId } from "../../common/cluster-types";
import { appEventBus } from "../../common/event-bus"; import { appEventBus } from "../../common/event-bus";
import { ipcMainHandle } from "../../common/ipc"; import { dialogShowOpenDialogHandler, ipcMainHandle } from "../../common/ipc";
import { catalogEntityRegistry } from "../catalog"; import { catalogEntityRegistry } from "../catalog";
import { ClusterManager } from "../cluster-manager"; import { ClusterManager } from "../cluster-manager";
import { bundledKubectlPath } from "../kubectl"; import { bundledKubectlPath } from "../kubectl";
import logger from "../logger"; import logger from "../logger";
import { promiseExecFile } from "../promise-exec"; import { promiseExecFile } from "../promise-exec";
import { ResourceApplier } from "../resource-applier"; import { ResourceApplier } from "../resource-applier";
import { WindowManager } from "../window-manager";
export function initIpcMainHandlers() { export function initIpcMainHandlers() {
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
@ -138,4 +139,10 @@ export function initIpcMainHandlers() {
throw `${clusterId} is not a valid cluster id`; throw `${clusterId} is not a valid cluster id`;
} }
}); });
ipcMainHandle(dialogShowOpenDialogHandler, async (event, dialogOpts: Electron.OpenDialogOptions) => {
await WindowManager.getInstance().ensureMainWindow();
return dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), dialogOpts);
});
} }

View File

@ -19,7 +19,6 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { app, remote } from "electron";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import { promiseExec } from "./promise-exec"; import { promiseExec } from "./promise-exec";
@ -32,6 +31,7 @@ import { customRequest } from "../common/request";
import { getBundledKubectlVersion } from "../common/utils/app-version"; import { getBundledKubectlVersion } from "../common/utils/app-version";
import { isDevelopment, isWindows, isTestEnv } from "../common/vars"; import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
import { SemVer } from "semver"; import { SemVer } from "semver";
import { getPath } from "../common/utils/getPath";
const bundledVersion = getBundledKubectlVersion(); const bundledVersion = getBundledKubectlVersion();
const kubectlMap: Map<string, string> = new Map([ const kubectlMap: Map<string, string> = new Map([
@ -84,7 +84,7 @@ export class Kubectl {
protected dirname: string; protected dirname: string;
static get kubectlDir() { static get kubectlDir() {
return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl"); return path.join(getPath("userData"), "binaries", "kubectl");
} }
public static readonly bundledKubectlVersion: string = bundledVersion; public static readonly bundledKubectlVersion: string = bundledVersion;

View File

@ -60,13 +60,13 @@ export class LensBinary {
this.logger = console; this.logger = console;
let arch = null; let arch = null;
if (process.arch == "x64") { if (process.env.BINARY_ARCH) {
arch = process.env.BINARY_ARCH;
} else if (process.arch == "x64") {
arch = "amd64"; arch = "amd64";
} } else if (process.arch == "x86" || process.arch == "ia32") {
else if (process.arch == "x86" || process.arch == "ia32") {
arch = "386"; arch = "386";
} } else {
else {
arch = process.arch; arch = process.arch;
} }
this.arch = arch; this.arch = arch;

View File

@ -83,7 +83,7 @@ export function buildMenu(windowManager: WindowManager) {
accelerator: "CmdOrCtrl+,", accelerator: "CmdOrCtrl+,",
click() { click() {
navigate(preferencesURL()); navigate(preferencesURL());
} },
}, },
{ {
label: "Extensions", label: "Extensions",
@ -106,7 +106,7 @@ export function buildMenu(windowManager: WindowManager) {
exitApp(); exitApp();
} }
} }
] ],
}; };
const fileMenu: MenuItemConstructorOptions = { const fileMenu: MenuItemConstructorOptions = {
label: "File", label: "File",
@ -150,7 +150,7 @@ export function buildMenu(windowManager: WindowManager) {
} }
} }
]) ])
] ],
}; };
const editMenu: MenuItemConstructorOptions = { const editMenu: MenuItemConstructorOptions = {
label: "Edit", label: "Edit",

View File

@ -89,7 +89,8 @@ export class WindowManager extends Singleton {
nodeIntegration: true, nodeIntegration: true,
nodeIntegrationInSubFrames: true, nodeIntegrationInSubFrames: true,
enableRemoteModule: true, enableRemoteModule: true,
webviewTag: true webviewTag: true,
contextIsolation: false,
}, },
}); });
this.windowState.manage(this.mainWindow); this.windowState.manage(this.mainWindow);
@ -257,7 +258,10 @@ export class WindowManager extends Singleton {
resizable: false, resizable: false,
show: false, show: false,
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: false,
nodeIntegrationInSubFrames: true,
} }
}); });
await this.splashWindow.loadURL("static://splash.html"); await this.splashWindow.loadURL("static://splash.html");

View File

@ -20,11 +20,11 @@
*/ */
import fse from "fs-extra"; import fse from "fs-extra";
import { app, remote } from "electron";
import path from "path"; import path from "path";
import { getPath } from "../../common/utils/getPath";
export function fileNameMigration() { export function fileNameMigration() {
const userDataPath = (app || remote.app).getPath("userData"); const userDataPath = getPath("userData");
const configJsonPath = path.join(userDataPath, "config.json"); const configJsonPath = path.join(userDataPath, "config.json");
const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json"); const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json");

View File

@ -30,6 +30,12 @@
box-sizing: content-box; box-sizing: content-box;
} }
.Select__option {
span.deprecated {
text-decoration: line-through;
}
}
.intro-contents { .intro-contents {
.description { .description {
font-weight: bold; font-weight: bold;

View File

@ -33,12 +33,19 @@ import { Button } from "../button";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { createInstallChartTab } from "../dock/install-chart.store"; import { createInstallChartTab } from "../dock/install-chart.store";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { Tooltip, withStyles } from "@material-ui/core";
interface Props { interface Props {
chart: HelmChart; chart: HelmChart;
hideDetails(): void; hideDetails(): void;
} }
const LargeTooltip = withStyles({
tooltip: {
fontSize: "var(--font-size-small)",
}
})(Tooltip);
@observer @observer
export class HelmChartDetails extends Component<Props> { export class HelmChartDetails extends Component<Props> {
@observable chartVersions: HelmChart[]; @observable chartVersions: HelmChart[];
@ -73,15 +80,15 @@ export class HelmChartDetails extends Component<Props> {
}); });
@boundMethod @boundMethod
async onVersionChange({ value: version }: SelectOption<string>) { async onVersionChange({ value: chart }: SelectOption<HelmChart>) {
this.selectedChart = this.chartVersions.find(chart => chart.version === version); this.selectedChart = chart;
this.readme = null; this.readme = null;
try { try {
this.abortController?.abort(); this.abortController?.abort();
this.abortController = new AbortController(); this.abortController = new AbortController();
const { chart: { name, repo } } = this.props; const { chart: { name, repo } } = this.props;
const { readme } = await getChartDetails(repo, name, { version, reqInit: { signal: this.abortController.signal }}); const { readme } = await getChartDetails(repo, name, { version: chart.version, reqInit: { signal: this.abortController.signal }});
this.readme = readme; this.readme = readme;
} catch (error) { } catch (error) {
@ -115,7 +122,19 @@ export class HelmChartDetails extends Component<Props> {
<Select <Select
themeName="outlined" themeName="outlined"
menuPortalTarget={null} menuPortalTarget={null}
options={chartVersions.map(chart => chart.version)} options={chartVersions.map(chart => ({
label: (
chart.deprecated
? (
<LargeTooltip title="Deprecated" placement="left">
<span className="deprecated">{chart.version}</span>
</LargeTooltip>
)
: chart.version
),
value: chart,
}))}
isOptionDisabled={({ value: chart }) => chart.deprecated}
value={selectedChart.getVersion()} value={selectedChart.getVersion()}
onChange={onVersionChange} onChange={onVersionChange}
/> />

View File

@ -96,7 +96,7 @@ export class HelmCharts extends Component<Props> {
searchProps: { searchProps: {
...searchProps, ...searchProps,
placeholder: "Search Helm Charts...", placeholder: "Search Helm Charts...",
}, }
})} })}
renderTableHeader={[ renderTableHeader={[
{ className: "icon", showWithColumn: columnId.name }, { className: "icon", showWithColumn: columnId.name },

View File

@ -25,11 +25,10 @@
.root { .root {
color: var(--textColorTertiary); color: var(--textColorTertiary);
min-height: 26px;
} }
.label { .label {
font-size: var(--font-size); font-size: var(--font-size)!important;
background-color: transparent!important; background-color: transparent!important;
&:hover { &:hover {

View File

@ -21,7 +21,7 @@
import "./extensions.scss"; import "./extensions.scss";
import { remote, shell } from "electron"; import { shell } from "electron";
import fse from "fs-extra"; import fse from "fs-extra";
import _ from "lodash"; import _ from "lodash";
import { makeObservable, observable, reaction, when } from "mobx"; import { makeObservable, observable, reaction, when } from "mobx";
@ -46,6 +46,8 @@ import { InstalledExtensions } from "./installed-extensions";
import { Notice } from "./notice"; import { Notice } from "./notice";
import { SettingLayout } from "../layout/setting-layout"; import { SettingLayout } from "../layout/setting-layout";
import { docsUrl } from "../../../common/vars"; import { docsUrl } from "../../../common/vars";
import { dialog } from "../../remote-helpers";
import { getPath } from "../../../common/utils/getPath";
function getMessageFromError(error: any): string { function getMessageFromError(error: any): string {
if (!error || typeof error !== "object") { if (!error || typeof error !== "object") {
@ -466,9 +468,8 @@ async function installFromInput(input: string) {
const supportedFormats = ["tar", "tgz"]; const supportedFormats = ["tar", "tgz"];
async function installFromSelectFileDialog() { async function installFromSelectFileDialog() {
const { dialog, BrowserWindow, app } = remote; const { canceled, filePaths } = await dialog.showOpenDialog({
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), { defaultPath: getPath("downloads"),
defaultPath: app.getPath("downloads"),
properties: ["openFile", "multiSelections"], properties: ["openFile", "multiSelections"],
message: `Select extensions to install (formats: ${supportedFormats.join(", ")}), `, message: `Select extensions to install (formats: ${supportedFormats.join(", ")}), `,
buttonLabel: "Use configuration", buttonLabel: "Use configuration",

View File

@ -45,3 +45,22 @@
} }
} }
} }
.NamespaceSelectFilterMenu {
right: 0;
.Select {
&__menu-list {
max-width: 400px;
}
&__option {
white-space: normal;
word-break: break-all;
}
}
.Icon {
margin-right: $margin / 2;
}
}

View File

@ -180,6 +180,7 @@ export class NamespaceSelectFilter extends React.Component<SelectProps> {
onBlur={this.reset} onBlur={this.reset}
formatOptionLabel={this.formatOptionLabel} formatOptionLabel={this.formatOptionLabel}
className="NamespaceSelectFilter" className="NamespaceSelectFilter"
menuClass="NamespaceSelectFilterMenu"
sort={(left, right) => +this.selected.has(right.value) - +this.selected.has(left.value)} sort={(left, right) => +this.selected.has(right.value) - +this.selected.has(left.value)}
/> />
</div> </div>

View File

@ -22,7 +22,7 @@
import "./add-helm-repo-dialog.scss"; import "./add-helm-repo-dialog.scss";
import React from "react"; import React from "react";
import { remote, FileFilter } from "electron"; import type { FileFilter } from "electron";
import { observable, makeObservable } from "mobx"; import { observable, makeObservable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Dialog, DialogProps } from "../dialog"; import { Dialog, DialogProps } from "../dialog";
@ -35,6 +35,7 @@ import { SubTitle } from "../layout/sub-title";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager"; import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager";
import { dialog } from "../../remote-helpers";
interface Props extends Partial<DialogProps> { interface Props extends Partial<DialogProps> {
onAddRepo: Function onAddRepo: Function
@ -88,8 +89,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
} }
async selectFileDialog(type: FileType, fileFilter: FileFilter) { async selectFileDialog(type: FileType, fileFilter: FileFilter) {
const { dialog, BrowserWindow } = remote; const { canceled, filePaths } = await dialog.showOpenDialog({
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
defaultPath: this.getFilePath(type), defaultPath: this.getFilePath(type),
properties: ["openFile", "showHiddenFiles"], properties: ["openFile", "showHiddenFiles"],
message: `Select file`, message: `Select file`,

View File

@ -50,6 +50,7 @@ export async function initView(clusterId: ClusterId) {
const parentElem = document.getElementById("lens-views"); const parentElem = document.getElementById("lens-views");
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
iframe.id = `cluster-frame-${cluster.id}`;
iframe.name = cluster.contextName; iframe.name = cluster.contextName;
iframe.setAttribute("src", getClusterFrameUrl(clusterId)); iframe.setAttribute("src", getClusterFrameUrl(clusterId));
iframe.addEventListener("load", () => { iframe.addEventListener("load", () => {

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export * from "./path-picker";

View File

@ -27,7 +27,7 @@ import { UserStore } from "../../common/user-store";
import { getAllEntries } from "../components/+preferences/kubeconfig-syncs"; import { getAllEntries } from "../components/+preferences/kubeconfig-syncs";
import { runInAction } from "mobx"; import { runInAction } from "mobx";
import { isWindows } from "../../common/vars"; import { isWindows } from "../../common/vars";
import { PathPicker } from "../components/path-picker/path-picker"; import { PathPicker } from "../components/path-picker";
import { Notifications } from "../components/notifications"; import { Notifications } from "../components/notifications";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";

View File

@ -117,7 +117,7 @@ function HotbarTooManyItemsHandler(): void {
Notifications.error(`Cannot have more than ${defaultHotbarCells} items pinned to a hotbar`); Notifications.error(`Cannot have more than ${defaultHotbarCells} items pinned to a hotbar`);
} }
export function registerIpcHandlers() { export function registerIpcListeners() {
onCorrect({ onCorrect({
source: ipcRenderer, source: ipcRenderer,
channel: UpdateAvailableChannel, channel: UpdateAvailableChannel,

View File

@ -0,0 +1,88 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ipcRenderer } from "electron";
import { navigate } from "../navigation";
/**
* The definition of a keyboard shortcut
*/
interface Shortcut {
code?: string;
key?: string;
metaKey?: boolean;
altKey?: boolean;
shiftKey?: boolean;
ctrlKey?: boolean;
action: () => void;
}
const shortcuts: Shortcut[] = [
{
key: "p",
metaKey: true,
shiftKey: true,
action: () => ipcRenderer.emit("command-palette:open"),
},
{
code: "Comma",
metaKey: true,
action: () => navigate("/preferences"),
},
];
function shortcutMatches(shortcut: Shortcut, event: KeyboardEvent): boolean {
if (typeof shortcut.metaKey === "boolean" && shortcut.metaKey !== event.metaKey) {
return false;
}
if (typeof shortcut.altKey === "boolean" && shortcut.altKey !== event.altKey) {
return false;
}
if (typeof shortcut.shiftKey === "boolean" && shortcut.shiftKey !== event.shiftKey) {
return false;
}
if (typeof shortcut.ctrlKey === "boolean" && shortcut.ctrlKey !== event.ctrlKey) {
return false;
}
if (typeof shortcut.code === "string" && shortcut.code !== event.code) {
return false;
}
if (typeof shortcut.key === "string" && shortcut.key !== event.key) {
return false;
}
return true;
}
export function registerKeyboardShortcuts() {
window.addEventListener("keydown", event => {
for (const shortcut of shortcuts) {
if (shortcutMatches(shortcut, event)) {
shortcut.action();
}
}
});
}

View File

@ -32,10 +32,11 @@ import { ExtensionLoader } from "../extensions/extension-loader";
import { broadcastMessage } from "../common/ipc"; import { broadcastMessage } from "../common/ipc";
import { CommandContainer } from "./components/command-palette/command-container"; import { CommandContainer } from "./components/command-palette/command-container";
import { bindProtocolAddRouteHandlers, LensProtocolRouterRenderer } from "./protocol-handler"; import { bindProtocolAddRouteHandlers, LensProtocolRouterRenderer } from "./protocol-handler";
import { registerIpcHandlers } from "./ipc"; import { registerIpcListeners } from "./ipc";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import { IpcRendererNavigationEvents } from "./navigation/events"; import { IpcRendererNavigationEvents } from "./navigation/events";
import { catalogEntityRegistry } from "./api/catalog-entity-registry"; import { catalogEntityRegistry } from "./api/catalog-entity-registry";
import { registerKeyboardShortcuts } from "./keyboard-shortcuts";
@observer @observer
export class LensApp extends React.Component { export class LensApp extends React.Component {
@ -48,7 +49,8 @@ export class LensApp extends React.Component {
window.addEventListener("offline", () => broadcastMessage("network:offline")); window.addEventListener("offline", () => broadcastMessage("network:offline"));
window.addEventListener("online", () => broadcastMessage("network:online")); window.addEventListener("online", () => broadcastMessage("network:online"));
registerIpcHandlers(); registerKeyboardShortcuts();
registerIpcListeners();
} }
componentDidMount() { componentDidMount() {

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { dialogShowOpenDialogHandler, requestMain } from "../../common/ipc";
export async function showOpenDialog(options: Electron.OpenDialogOptions): Promise<Electron.OpenDialogReturnValue> {
return requestMain(dialogShowOpenDialogHandler, options);
}

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import * as dialog from "./dialog";
export { dialog };

View File

@ -22,13 +22,12 @@
// Keeps window.localStorage state in external JSON-files. // Keeps window.localStorage state in external JSON-files.
// Because app creates random port between restarts => storage session wiped out each time. // Because app creates random port between restarts => storage session wiped out each time.
import path from "path"; import path from "path";
import { app, remote } from "electron";
import { comparer, observable, reaction, toJS, when } from "mobx"; import { comparer, observable, reaction, toJS, when } from "mobx";
import fse from "fs-extra"; import fse from "fs-extra";
import { StorageHelper } from "./storageHelper"; import { StorageHelper } from "./storageHelper";
import { ClusterStore } from "../../common/cluster-store"; import { ClusterStore } from "../../common/cluster-store";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { getHostedClusterId } from "../../common/utils"; import { getHostedClusterId, getPath } from "../../common/utils";
const storage = observable({ const storage = observable({
initialized: false, initialized: false,
@ -47,7 +46,7 @@ export function createStorage<T>(key: string, defaultValue: T) {
export function createAppStorage<T>(key: string, defaultValue: T, clusterId?: string | undefined) { export function createAppStorage<T>(key: string, defaultValue: T, clusterId?: string | undefined) {
const { logPrefix } = StorageHelper; const { logPrefix } = StorageHelper;
const folder = path.resolve((app || remote.app).getPath("userData"), "lens-local-storage"); const folder = path.resolve(getPath("userData"), "lens-local-storage");
const fileName = `${clusterId ?? "app"}.json`; const fileName = `${clusterId ?? "app"}.json`;
const filePath = path.resolve(folder, fileName); const filePath = path.resolve(folder, fileName);

View File

@ -82,7 +82,6 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
optimization: { optimization: {
minimize: false minimize: false
}, },
module: { module: {
rules: [ rules: [
{ {

542
yarn.lock

File diff suppressed because it is too large Load Diff