Merge remote-tracking branch 'origin/master' into lens_restructure
# Conflicts: # package.json # src/main/index.ts # src/renderer/api/__test__/parseAPI.test.ts # src/renderer/components/chart/bar-chart.tsx # src/renderer/components/chart/chart.tsx # src/renderer/components/chart/pie-chart.tsx # src/renderer/utils/__tests__/arrays.test.ts # src/renderer/utils/arrays.ts # yarn.lock
@ -13,7 +13,6 @@ trigger:
|
||||
- "*"
|
||||
jobs:
|
||||
- job: Windows
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
pool:
|
||||
vmImage: windows-2019
|
||||
strategy:
|
||||
@ -34,10 +33,14 @@ jobs:
|
||||
inputs:
|
||||
key: yarn | $(Agent.OS) | yarn.lock
|
||||
path: $(YARN_CACHE_FOLDER)
|
||||
cacheHitVar: CACHE_RESTORED
|
||||
displayName: Cache Yarn packages
|
||||
- script: make deps
|
||||
displayName: Install dependencies
|
||||
- script: make integration-win
|
||||
displayName: Run integration tests
|
||||
- script: make build
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
displayName: Build
|
||||
env:
|
||||
WIN_CSC_LINK: $(WIN_CSC_LINK)
|
||||
@ -53,6 +56,7 @@ jobs:
|
||||
steps:
|
||||
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
||||
displayName: Set the tag name as an environment variable
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: $(node_version)
|
||||
@ -138,6 +142,7 @@ jobs:
|
||||
SNAP_LOGIN: $(SNAP_LOGIN)
|
||||
- script: make build
|
||||
displayName: Build
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
env:
|
||||
GH_TOKEN: $(GH_TOKEN)
|
||||
- bash: |
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# Lens | The Kubernetes IDE
|
||||
|
||||
[](https://dev.azure.com/lensapp/lensapp/_build/latest?definitionId=1&branchName=master)
|
||||
[](https://github.com/lensapp/lens/releases)
|
||||
[](https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI)
|
||||
|
||||
Lens is the only IDE you’ll ever need to take control of your Kubernetes clusters. It is a standalone application for MacOS, Windows and Linux operating systems. It is open source and free.
|
||||
|
||||
|
||||
BIN
build/icon.ico
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 116 KiB |
BIN
build/icon.png
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 26 KiB |
@ -4,6 +4,12 @@
|
||||
"description": "Lens - The Kubernetes IDE",
|
||||
"version": "3.5.0-beta.1",
|
||||
"main": "dist/main.js",
|
||||
"copyright": "© 2020, Lakend Labs, Inc.",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Lakend Labs, Inc.",
|
||||
"email": "info@lakendlabs.com"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "concurrently 'yarn dev:main' 'yarn dev:renderer'",
|
||||
"dev-run": "electron --inspect .",
|
||||
@ -30,9 +36,6 @@
|
||||
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
||||
"download:helm": "yarn run ts-node build/download_helm.ts"
|
||||
},
|
||||
"author": "Lakend Labs, Inc. <info@lakendlabs.com>",
|
||||
"copyright": "© 2020, Lakend Labs, Inc.",
|
||||
"license": "MIT",
|
||||
"config": {
|
||||
"bundledKubectlVersion": "1.17.4",
|
||||
"bundledHelmVersion": "3.1.2"
|
||||
|
||||
@ -3,7 +3,7 @@ import { Application } from "spectron";
|
||||
let appPath = ""
|
||||
switch(process.platform) {
|
||||
case "win32":
|
||||
appPath = "./dist/win-unpacked/Lens.exe"
|
||||
appPath = "./dist/win-unpacked/LensDev.exe"
|
||||
break
|
||||
case "linux":
|
||||
appPath = "./dist/linux-unpacked/kontena-lens"
|
||||
|
||||
@ -5,6 +5,8 @@ import { stat } from "fs"
|
||||
|
||||
jest.setTimeout(20000)
|
||||
|
||||
const BACKSPACE = "\uE003"
|
||||
|
||||
describe("app start", () => {
|
||||
let app: Application
|
||||
const clickWhatsNew = async (app: Application) => {
|
||||
@ -13,6 +15,24 @@ describe("app start", () => {
|
||||
await app.client.waitUntilTextExists("h1", "Welcome")
|
||||
}
|
||||
|
||||
const addMinikubeCluster = async (app: Application) => {
|
||||
await app.client.click("a#add-cluster")
|
||||
await app.client.waitUntilTextExists("legend", "Choose config:")
|
||||
await app.client.selectByVisibleText("select#kubecontext-select", "minikube (new)")
|
||||
await app.client.click("button.btn-primary")
|
||||
}
|
||||
|
||||
const waitForMinikubeDashboard = async (app: Application) => {
|
||||
await app.client.waitUntilTextExists("pre.auth-output", "Authentication proxy started")
|
||||
let windowCount = await app.client.getWindowCount()
|
||||
// wait for webview to appear on window count
|
||||
while (windowCount == 1) {
|
||||
windowCount = await app.client.getWindowCount()
|
||||
}
|
||||
await app.client.windowByIndex(windowCount - 1)
|
||||
await app.client.waitUntilTextExists("span.link-text", "Cluster")
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
app = util.setup()
|
||||
await app.start()
|
||||
@ -32,22 +52,48 @@ describe("app start", () => {
|
||||
return
|
||||
}
|
||||
await clickWhatsNew(app)
|
||||
await app.client.click("a#add-cluster")
|
||||
await app.client.waitUntilTextExists("legend", "Choose config:")
|
||||
await app.client.selectByVisibleText("select#kubecontext-select", "minikube (new)")
|
||||
await app.client.click("button.btn-primary")
|
||||
await app.client.waitUntilTextExists("pre.auth-output", "Authentication proxy started")
|
||||
let windowCount = await app.client.getWindowCount()
|
||||
// wait for webview to appear on window count
|
||||
while (windowCount == 1) {
|
||||
windowCount = await app.client.getWindowCount()
|
||||
}
|
||||
await app.client.windowByIndex(windowCount - 1)
|
||||
await app.client.waitUntilTextExists("span.link-text", "Cluster")
|
||||
await addMinikubeCluster(app)
|
||||
await waitForMinikubeDashboard(app)
|
||||
await app.client.click('a[href="/nodes"]')
|
||||
await app.client.waitUntilTextExists("div.TableCell", "minikube")
|
||||
})
|
||||
|
||||
it('allows to create a pod', async () => {
|
||||
const status = spawnSync("minikube status", {shell: true})
|
||||
if (status.status !== 0) {
|
||||
console.warn("minikube not running, skipping test")
|
||||
return
|
||||
}
|
||||
await clickWhatsNew(app)
|
||||
await addMinikubeCluster(app)
|
||||
await waitForMinikubeDashboard(app)
|
||||
await app.client.click(".sidebar-nav #workloads span.link-text")
|
||||
await app.client.waitUntilTextExists('a[href="/pods"]', "Pods")
|
||||
await app.client.click('a[href="/pods"]')
|
||||
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver-minikube")
|
||||
await app.client.click('.Icon.new-dock-tab')
|
||||
await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource")
|
||||
await app.client.click("li.MenuItem.create-resource-tab")
|
||||
await app.client.waitForVisible(".CreateResource div.ace_content")
|
||||
// Write pod manifest to editor
|
||||
await app.client.keys("apiVersion: v1\n")
|
||||
await app.client.keys("kind: Pod\n")
|
||||
await app.client.keys("metadata:\n")
|
||||
await app.client.keys(" name: nginx\n")
|
||||
await app.client.keys(BACKSPACE + "spec:\n")
|
||||
await app.client.keys(" containers:\n")
|
||||
await app.client.keys("- name: nginx\n")
|
||||
await app.client.keys(" image: nginx:alpine\n")
|
||||
// Create deployent
|
||||
await app.client.waitForEnabled("button.Button=Create & Close")
|
||||
await app.client.click("button.Button=Create & Close")
|
||||
// Wait until first bits of pod appears on dashboard
|
||||
await app.client.waitForExist(".name=nginx")
|
||||
// Open pod details
|
||||
await app.client.click(".name=nginx")
|
||||
await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx")
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (app && app.isRunning()) {
|
||||
return util.tearDown(app)
|
||||
|
||||
@ -29,6 +29,7 @@ if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||
const promiseIpc = new PromiseIpc({ timeout: 2000 })
|
||||
let windowManager: WindowManager = null;
|
||||
let clusterManager: ClusterManager = null;
|
||||
let proxyServer: proxy.LensProxy = null;
|
||||
|
||||
const vmURL = formatUrl({
|
||||
pathname: path.join(__dirname, `${vueAppName}.html`),
|
||||
@ -64,10 +65,9 @@ async function main() {
|
||||
|
||||
// create cluster manager
|
||||
clusterManager = new ClusterManager(clusterStore.getAllClusterObjects(), port)
|
||||
|
||||
// run proxy
|
||||
try {
|
||||
proxy.listen(port, clusterManager)
|
||||
proxyServer = proxy.listen(port, clusterManager)
|
||||
} catch (error) {
|
||||
logger.error(`Could not start proxy (127.0.0:${port}): ${error.message}`)
|
||||
await dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${port}): ${error.message || "unknown error"}`)
|
||||
@ -87,7 +87,7 @@ async function main() {
|
||||
},
|
||||
showPreferencesHook: async () => {
|
||||
// IPC send needs webContents as we're sending it to renderer
|
||||
promiseIpc.send('navigate', findMainWebContents(), { name: 'preferences-page' }).then((data: any) => {
|
||||
promiseIpc.send('navigate', findMainWebContents(), {name: 'preferences-page'}).then((data: any) => {
|
||||
logger.debug("navigate: preferences IPC sent");
|
||||
})
|
||||
},
|
||||
@ -110,10 +110,10 @@ async function main() {
|
||||
}
|
||||
|
||||
app.on("ready", main)
|
||||
app.on('window-all-closed', function () {
|
||||
app.on('window-all-closed', function() {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (!isMac) {
|
||||
if (process.platform != 'darwin') {
|
||||
app.quit();
|
||||
} else {
|
||||
windowManager = null
|
||||
@ -130,5 +130,6 @@ app.on("activate", () => {
|
||||
app.on("will-quit", async (event) => {
|
||||
event.preventDefault(); // To allow mixpanel sending to be executed
|
||||
if (clusterManager) clusterManager.stop()
|
||||
if (proxyServer) proxyServer.close()
|
||||
app.exit(0);
|
||||
})
|
||||
|
||||
@ -18,7 +18,7 @@ export class PrometheusLens implements PrometheusProvider {
|
||||
port: service.spec.ports[0].port
|
||||
}
|
||||
} catch(error) {
|
||||
logger.warn(`PrometheusLens: failed to list services: ${error.toString()}`)
|
||||
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,8 @@ export class LensProxy {
|
||||
protected clusterManager: ClusterManager
|
||||
protected retryCounters: Map<string, number> = new Map()
|
||||
protected router: Router
|
||||
protected proxyServer: http.Server
|
||||
protected closed = false
|
||||
|
||||
constructor(port: number, clusterManager: ClusterManager) {
|
||||
this.port = port
|
||||
@ -28,6 +30,13 @@ export class LensProxy {
|
||||
public run() {
|
||||
const proxyServer = this.buildProxyServer();
|
||||
proxyServer.listen(this.port, "127.0.0.1")
|
||||
this.proxyServer = proxyServer
|
||||
}
|
||||
|
||||
public close() {
|
||||
logger.info("Closing proxy server")
|
||||
this.proxyServer.close()
|
||||
this.closed = true
|
||||
}
|
||||
|
||||
protected buildProxyServer() {
|
||||
@ -68,6 +77,9 @@ export class LensProxy {
|
||||
}
|
||||
})
|
||||
proxy.on("error", (error, req, res, target) => {
|
||||
if(this.closed) {
|
||||
return
|
||||
}
|
||||
if (target) {
|
||||
logger.debug("Failed proxy to target: " + JSON.stringify(target))
|
||||
if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {
|
||||
|
||||
@ -11,7 +11,7 @@ import { helmCli } from "./helm-cli"
|
||||
import { isWindows } from "../common/vars";
|
||||
|
||||
export class ShellSession extends EventEmitter {
|
||||
static shellEnv: any
|
||||
static shellEnvs: Map<string, any> = new Map()
|
||||
|
||||
protected websocket: WebSocket
|
||||
protected shellProcess: pty.IPty
|
||||
@ -22,6 +22,7 @@ export class ShellSession extends EventEmitter {
|
||||
protected helmBinDir: string;
|
||||
protected preferences: ClusterPreferences;
|
||||
protected running = false;
|
||||
protected clusterId: string;
|
||||
|
||||
constructor(socket: WebSocket, pathToKubeconfig: string, cluster: Cluster) {
|
||||
super()
|
||||
@ -29,6 +30,7 @@ export class ShellSession extends EventEmitter {
|
||||
this.kubeconfigPath = pathToKubeconfig
|
||||
this.kubectl = new Kubectl(cluster.version)
|
||||
this.preferences = cluster.preferences || {}
|
||||
this.clusterId = cluster.id
|
||||
}
|
||||
|
||||
public async open() {
|
||||
@ -77,16 +79,14 @@ export class ShellSession extends EventEmitter {
|
||||
}
|
||||
|
||||
protected async getCachedShellEnv() {
|
||||
let env: any
|
||||
if (!ShellSession.shellEnv) {
|
||||
let env = ShellSession.shellEnvs.get(this.clusterId)
|
||||
if (!env) {
|
||||
env = await this.getShellEnv()
|
||||
ShellSession.shellEnv = env
|
||||
ShellSession.shellEnvs.set(this.clusterId, env)
|
||||
} else {
|
||||
env = ShellSession.shellEnv
|
||||
|
||||
// refresh env in the background
|
||||
this.getShellEnv().then((shellEnv: any) => {
|
||||
ShellSession.shellEnv = shellEnv
|
||||
ShellSession.shellEnvs.set(this.clusterId, shellEnv)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -122,7 +122,6 @@
|
||||
id="checkbox-allow-telemetry"
|
||||
switch
|
||||
v-model="preferences.allowTelemetry"
|
||||
:disabled="licenceData && licenceData.status === 'valid'"
|
||||
@input="onSave"
|
||||
>
|
||||
Allow telemetry & usage tracking
|
||||
|
||||
@ -47,6 +47,11 @@ export default new Vuex.Store({
|
||||
this.commit("savePreferences", userStore.getPreferences());
|
||||
},
|
||||
savePreferences(state, prefs) {
|
||||
if (prefs.allowTelemetry) {
|
||||
tracker.event("telemetry", "enabled")
|
||||
} else {
|
||||
tracker.event("telemetry", "disabled")
|
||||
}
|
||||
state.preferences = prefs;
|
||||
userStore.setPreferences(prefs);
|
||||
this.dispatch("destroyWebviews")
|
||||
|
||||
0
src/renderer/api/__test__/parseAPI.test.ts
Normal file
@ -156,9 +156,12 @@ export class HelmRelease implements ItemObject {
|
||||
}
|
||||
|
||||
getChart(withVersion = false) {
|
||||
return withVersion ?
|
||||
this.chart :
|
||||
this.chart.substr(0, this.chart.lastIndexOf("-"));
|
||||
let chart = this.chart
|
||||
if(!withVersion && this.getVersion() != "" ) {
|
||||
const search = new RegExp(`-${this.getVersion()}`)
|
||||
chart = chart.replace(search, "");
|
||||
}
|
||||
return chart
|
||||
}
|
||||
|
||||
getRevision() {
|
||||
@ -170,7 +173,7 @@ export class HelmRelease implements ItemObject {
|
||||
}
|
||||
|
||||
getVersion() {
|
||||
const versions = this.chart.match(/(\d+)[^-]*$/)
|
||||
const versions = this.chart.match(/(v?\d+)[^-].*$/)
|
||||
if (versions) {
|
||||
return versions[0]
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { autobind } from "../../utils";
|
||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { IPodContainer } from "./pods.api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { JsonApiParams } from "../json-api";
|
||||
|
||||
@autobind()
|
||||
export class Job extends WorkloadKubeObject {
|
||||
@ -88,6 +89,13 @@ export class Job extends WorkloadKubeObject {
|
||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", [])
|
||||
return [...containers].map(container => container.image)
|
||||
}
|
||||
|
||||
delete() {
|
||||
const params: JsonApiParams = {
|
||||
query: { propagationPolicy: "Background" }
|
||||
}
|
||||
return super.delete(params)
|
||||
}
|
||||
}
|
||||
|
||||
export const jobApi = new KubeApi({
|
||||
|
||||
@ -56,7 +56,21 @@ export const metricsApi = {
|
||||
};
|
||||
|
||||
export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
|
||||
if (!metrics?.data?.result) {
|
||||
return {
|
||||
data: {
|
||||
resultType: "",
|
||||
result: [{
|
||||
metric: {},
|
||||
values: []
|
||||
} as IMetricsResult],
|
||||
},
|
||||
status: "",
|
||||
}
|
||||
}
|
||||
|
||||
const { result } = metrics.data;
|
||||
|
||||
if (result.length) {
|
||||
if (frames > 0) {
|
||||
// fill the gaps
|
||||
@ -81,13 +95,16 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
|
||||
}
|
||||
|
||||
export function isMetricsEmpty(metrics: { [key: string]: IMetrics }) {
|
||||
return Object.values(metrics).every(metric => !metric.data.result.length);
|
||||
return Object.values(metrics).every(metric => !metric?.data?.result?.length);
|
||||
}
|
||||
|
||||
export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: string) {
|
||||
export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: string): { [key: string]: IMetrics } {
|
||||
if (!metrics) return;
|
||||
const itemMetrics = { ...metrics };
|
||||
for (const metric in metrics) {
|
||||
if (!metrics[metric]?.data?.result) {
|
||||
continue
|
||||
}
|
||||
const results = metrics[metric].data.result;
|
||||
const result = results.find(res => Object.values(res.metric)[0] == itemName);
|
||||
itemMetrics[metric].data.result = result ? [result] : [];
|
||||
|
||||
@ -7,6 +7,7 @@ import { IKubeObjectRef, KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } fro
|
||||
import { apiKube } from "./index";
|
||||
import { kubeWatchApi } from "./kube-watch-api";
|
||||
import { apiManager } from "./api-manager";
|
||||
import { split } from "../utils/arrays";
|
||||
|
||||
export interface IKubeApiOptions<T extends KubeObject> {
|
||||
kind: string; // resource type within api-group, e.g. "Namespace"
|
||||
@ -32,20 +33,45 @@ export interface IKubeApiLinkRef {
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export class KubeApi<T extends KubeObject = any> {
|
||||
static matcher = /(\/apis?.*?)\/(?:(.*?)\/)?(v.*?)(?:\/namespaces\/(.+?))?\/([^\/]+)(?:\/([^\/?]+))?.*$/
|
||||
export interface IKubeApiLinkBase extends IKubeApiLinkRef {
|
||||
apiBase: string;
|
||||
apiGroup: string;
|
||||
apiVersionWithGroup: string;
|
||||
}
|
||||
|
||||
static parseApi(apiPath = "") {
|
||||
export class KubeApi<T extends KubeObject = any> {
|
||||
static parseApi(apiPath = ""): IKubeApiLinkBase {
|
||||
apiPath = new URL(apiPath, location.origin).pathname;
|
||||
const [, apiPrefix, apiGroup = "", apiVersion, namespace, resource, name] = apiPath.match(KubeApi.matcher) || [];
|
||||
const [, prefix, ...parts] = apiPath.split("/");
|
||||
const apiPrefix = `/${prefix}`;
|
||||
|
||||
const [left, right, found] = split(parts, "namespaces");
|
||||
let apiGroup, apiVersion, namespace, resource, name;
|
||||
|
||||
if (found) {
|
||||
if (left.length == 0) {
|
||||
throw new Error(`invalid apiPath: ${apiPath}`)
|
||||
}
|
||||
|
||||
apiVersion = left.pop();
|
||||
apiGroup = left.join("/");
|
||||
[namespace, resource, name] = right;
|
||||
} else {
|
||||
[apiGroup, apiVersion, resource] = left;
|
||||
}
|
||||
const apiVersionWithGroup = [apiGroup, apiVersion].filter(v => v).join("/");
|
||||
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
|
||||
|
||||
if (!apiBase) {
|
||||
throw new Error(`invalid apiPath: ${apiPath}`)
|
||||
}
|
||||
|
||||
return {
|
||||
apiBase,
|
||||
apiPrefix, apiGroup,
|
||||
apiVersion, apiVersionWithGroup,
|
||||
namespace, resource, name,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static createLink(ref: IKubeApiLinkRef): string {
|
||||
@ -55,7 +81,7 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
namespace = `namespaces/${namespace}`
|
||||
}
|
||||
return [apiPrefix, apiVersion, namespace, resource, name]
|
||||
.filter(v => !!v)
|
||||
.filter(v => v)
|
||||
.join("/")
|
||||
}
|
||||
|
||||
@ -130,8 +156,9 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
if (KubeObject.isJsonApiData(data)) {
|
||||
return new KubeObjectConstructor(data);
|
||||
}
|
||||
|
||||
// process items list response
|
||||
else if (KubeObject.isJsonApiDataList(data)) {
|
||||
if (KubeObject.isJsonApiDataList(data)) {
|
||||
const { apiVersion, items, metadata } = data;
|
||||
this.setResourceVersion(namespace, metadata.resourceVersion);
|
||||
this.setResourceVersion("", metadata.resourceVersion);
|
||||
@ -141,10 +168,12 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
...item,
|
||||
}))
|
||||
}
|
||||
|
||||
// custom apis might return array for list response, e.g. users, groups, etc.
|
||||
else if (Array.isArray(data)) {
|
||||
if (Array.isArray(data)) {
|
||||
return data.map(data => new KubeObjectConstructor(data));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@ -162,16 +191,19 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
|
||||
async create({ name = "", namespace = "default" } = {}, data?: Partial<T>): Promise<T> {
|
||||
const apiUrl = this.getUrl({ namespace });
|
||||
return this.request.post(apiUrl, {
|
||||
data: merge({
|
||||
kind: this.kind,
|
||||
apiVersion: this.apiVersionWithGroup,
|
||||
metadata: {
|
||||
name,
|
||||
namespace
|
||||
}
|
||||
}, data)
|
||||
}).then(this.parseResponse);
|
||||
|
||||
return this.request
|
||||
.post(apiUrl, {
|
||||
data: merge({
|
||||
kind: this.kind,
|
||||
apiVersion: this.apiVersionWithGroup,
|
||||
metadata: {
|
||||
name,
|
||||
namespace
|
||||
}
|
||||
}, data)
|
||||
})
|
||||
.then(this.parseResponse);
|
||||
}
|
||||
|
||||
async update({ name = "", namespace = "default" } = {}, data?: Partial<T>): Promise<T> {
|
||||
|
||||
@ -5,6 +5,7 @@ import { KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api";
|
||||
import { autobind, formatDuration } from "../utils";
|
||||
import { ItemObject } from "../item.store";
|
||||
import { apiKube } from "./index";
|
||||
import { JsonApiParams } from "./json-api";
|
||||
import { resourceApplierApi } from "./endpoints/resource-applier.api";
|
||||
|
||||
export type IKubeObjectConstructor<T extends KubeObject = any> = (new (data: KubeJsonApiData | any) => T) & {
|
||||
@ -153,7 +154,7 @@ export class KubeObject implements ItemObject {
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
return apiKube.del(this.selfLink);
|
||||
delete(params?: JsonApiParams) {
|
||||
return apiKube.del(this.selfLink, params);
|
||||
}
|
||||
}
|
||||
@ -82,15 +82,15 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
|
||||
this.liveMetrics = await this.loadMetrics({ start, end, step, range });
|
||||
}
|
||||
|
||||
getMetricsValues(source: Partial<IClusterMetrics>) {
|
||||
const metrics =
|
||||
this.metricType === MetricType.CPU ? source.cpuUsage :
|
||||
this.metricType === MetricType.MEMORY ? source.memoryUsage
|
||||
: null;
|
||||
if (!metrics) {
|
||||
getMetricsValues(source: Partial<IClusterMetrics>): [number, string][] {
|
||||
switch (this.metricType) {
|
||||
case MetricType.CPU:
|
||||
return normalizeMetrics(source.cpuUsage).data.result[0].values
|
||||
case MetricType.MEMORY:
|
||||
return normalizeMetrics(source.memoryUsage).data.result[0].values
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
return normalizeMetrics(metrics).data.result[0].values;
|
||||
}
|
||||
|
||||
resetMetrics() {
|
||||
|
||||
@ -34,7 +34,7 @@ export class NamespaceSelect extends React.Component<Props> {
|
||||
private unsubscribe = noop;
|
||||
|
||||
async componentDidMount() {
|
||||
if (isAllowedResource("namespaces") && !namespaceStore.isLoaded) {
|
||||
if (!namespaceStore.isLoaded) {
|
||||
await namespaceStore.loadAll();
|
||||
}
|
||||
this.unsubscribe = namespaceStore.subscribe();
|
||||
|
||||
@ -4,6 +4,7 @@ import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { Namespace, namespacesApi } from "../../api/endpoints";
|
||||
import { IQueryParams, navigation, setQueryParams } from "../../navigation";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { isAllowedResource } from "../..//api/rbac";
|
||||
|
||||
@autobind()
|
||||
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
@ -43,6 +44,16 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
}
|
||||
|
||||
protected loadItems(namespaces?: string[]) {
|
||||
if (!isAllowedResource("namespaces")) {
|
||||
if (namespaces) {
|
||||
return Promise.all(namespaces.map(name => this.getDummyNamespace(name)))
|
||||
}
|
||||
else {
|
||||
return new Promise<Namespace[]>(() => {
|
||||
return []
|
||||
})
|
||||
}
|
||||
}
|
||||
if (namespaces) {
|
||||
return Promise.all(namespaces.map(name => this.api.get({ name })))
|
||||
}
|
||||
@ -51,6 +62,19 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
}
|
||||
}
|
||||
|
||||
protected getDummyNamespace(name: string) {
|
||||
return new Namespace({
|
||||
kind: "Namespace",
|
||||
apiVersion: "v1",
|
||||
metadata: {
|
||||
name: name,
|
||||
uid: "",
|
||||
resourceVersion: "",
|
||||
selfLink: `/api/v1/namespaces/${name}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setContext(namespaces: string[]) {
|
||||
this.contextNs.replace(namespaces);
|
||||
}
|
||||
|
||||
@ -18,9 +18,9 @@ export const IngressCharts = observer(() => {
|
||||
if (!metrics) return null;
|
||||
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
|
||||
|
||||
const values = Object.values(metrics).map(metric =>
|
||||
normalizeMetrics(metric).data.result[0].values
|
||||
);
|
||||
const values = Object.values(metrics)
|
||||
.map(normalizeMetrics)
|
||||
.map(({ data }) => data.result[0].values);
|
||||
const [
|
||||
bytesSentSuccess,
|
||||
bytesSentFailure,
|
||||
|
||||
@ -96,7 +96,7 @@ export class DeploymentScaleDialog extends Component<Props> {
|
||||
<Trans>Desired number of replicas</Trans>: {desiredReplicas}
|
||||
</div>
|
||||
<div className="slider-container">
|
||||
<Slider value={desiredReplicas} max={scaleMax} onChange={onChange}/>
|
||||
<Slider value={desiredReplicas} max={scaleMax} onChange={onChange as any /** see: https://github.com/mui-org/material-ui/issues/20191 */}/>
|
||||
</div>
|
||||
</div>
|
||||
{warning &&
|
||||
|
||||
@ -17,9 +17,9 @@ export const ContainerCharts = () => {
|
||||
if (!metrics) return null;
|
||||
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
|
||||
|
||||
const values = Object.values(metrics).map(metric =>
|
||||
normalizeMetrics(metric).data.result[0].values
|
||||
);
|
||||
const values = Object.values(metrics)
|
||||
.map(normalizeMetrics)
|
||||
.map(({ data }) => data.result[0].values);
|
||||
const [
|
||||
cpuUsage,
|
||||
cpuRequests,
|
||||
|
||||
@ -28,9 +28,9 @@ export const PodCharts = observer(() => {
|
||||
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
|
||||
|
||||
const options = tabId == 0 ? cpuOptions : memoryOptions;
|
||||
const values = Object.values(metrics).map(metric =>
|
||||
normalizeMetrics(metric).data.result[0].values
|
||||
);
|
||||
const values = Object.values(metrics)
|
||||
.map(normalizeMetrics)
|
||||
.map(({ data }) => data.result[0].values);
|
||||
const [
|
||||
cpuUsage,
|
||||
cpuRequests,
|
||||
|
||||
@ -120,8 +120,11 @@ const SecretKey = (props: SecretKeyProps) => {
|
||||
setSecret(secret)
|
||||
}
|
||||
|
||||
if (!secret) {
|
||||
return (
|
||||
if (secret?.data?.[key]) {
|
||||
return <>{base64.decode(secret.data[key])}</>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
secretKeyRef({name}.{key})
|
||||
<Icon
|
||||
@ -131,7 +134,5 @@ const SecretKey = (props: SecretKeyProps) => {
|
||||
onClick={showKey}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return <>{base64.decode(secret.data[key])}</>
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -139,7 +139,7 @@ export function BarChart(props: Props) {
|
||||
}
|
||||
};
|
||||
const options = merge(barOptions, customOptions);
|
||||
if (!chartData.datasets.length) {
|
||||
if (chartData.datasets.length == 0) {
|
||||
return <NoMetrics/>
|
||||
}
|
||||
return (
|
||||
@ -159,10 +159,15 @@ export const memoryOptions: ChartOptions = {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
callback: value => {
|
||||
value = parseFloat(String(value));
|
||||
if (!value) return 0;
|
||||
return value < 1 ? value.toFixed(3) : bytesToUnits(value);
|
||||
callback: (value: number | string): string => {
|
||||
if (typeof value == "string") {
|
||||
const float = parseFloat(value);
|
||||
if (float < 1) {
|
||||
return float.toFixed(3);
|
||||
}
|
||||
return bytesToUnits(parseInt(value));
|
||||
}
|
||||
return `${value}`;
|
||||
},
|
||||
stepSize: 1
|
||||
}
|
||||
@ -184,11 +189,12 @@ export const cpuOptions: ChartOptions = {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
callback: (value: number) => {
|
||||
if (value == 0) return 0;
|
||||
if (value < 10) return value.toFixed(3);
|
||||
if (value < 100) return value.toFixed(2);
|
||||
return value.toFixed(1);
|
||||
callback: (value: number | string): string => {
|
||||
const float = parseFloat(`${value}`);
|
||||
if (float == 0) return "0";
|
||||
if (float < 10) return float.toFixed(3);
|
||||
if (float < 100) return float.toFixed(2);
|
||||
return float.toFixed(1);
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import "./chart.scss";
|
||||
import React from "react";
|
||||
import ChartJS from "chart.js";
|
||||
import ChartJS, {ChartData, ChartOptions} from "chart.js";
|
||||
import { isEqual, remove } from "lodash";
|
||||
import { cssNames } from "../../utils";
|
||||
import { StatusBrick } from "../status-brick";
|
||||
@ -17,7 +17,7 @@ export interface ChartDataSets extends ChartJS.ChartDataSets {
|
||||
|
||||
export interface ChartProps {
|
||||
data: ChartData;
|
||||
options?: ChartJS.ChartOptions; // Passed to ChartJS instance
|
||||
options?: ChartOptions; // Passed to ChartJS instance
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
type?: ChartKind;
|
||||
|
||||
@ -5,7 +5,10 @@ import { Chart, ChartProps } from "./chart";
|
||||
import { cssNames } from "../../utils";
|
||||
import { themeStore } from "../../theme.store";
|
||||
|
||||
export class PieChart extends React.Component<ChartProps> {
|
||||
interface Props extends ChartProps {
|
||||
}
|
||||
|
||||
export class PieChart extends React.Component<Props> {
|
||||
render() {
|
||||
const { data, className, options, ...chartProps } = this.props
|
||||
const { contentColor } = themeStore.activeTheme.colors;
|
||||
|
||||
@ -119,12 +119,12 @@ export class Dock extends React.Component<Props> {
|
||||
/>
|
||||
<div className="toolbar flex gaps align-center box grow">
|
||||
<div className="dock-menu box grow">
|
||||
<MenuActions usePortal triggerIcon={{ material: "add", tooltip: <Trans>New tab</Trans> }} closeOnScroll={false}>
|
||||
<MenuItem onClick={() => createTerminalTab()}>
|
||||
<MenuActions usePortal triggerIcon={{ material: "add", className: "new-dock-tab", tooltip: <Trans>New tab</Trans> }} closeOnScroll={false}>
|
||||
<MenuItem className="create-terminal-tab" onClick={() => createTerminalTab()}>
|
||||
<Icon small svg="terminal" size={15}/>
|
||||
<Trans>Terminal session</Trans>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => createResourceTab()}>
|
||||
<MenuItem className="create-resource-tab" onClick={() => createResourceTab()}>
|
||||
<Icon small material="create"/>
|
||||
<Trans>Create resource</Trans>
|
||||
</MenuItem>
|
||||
|
||||
@ -1,161 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"> <image id="image0" width="512" height="512" x="0" y="0"
|
||||
href="data:image/png;base64,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
|
||||
1XPH56f6Avtuvpj+ovel5OXgq4K/1P7a/try9fG/ff6+3p/Q3/tG+mbo7Zp32u/2v5/8vnkgYuDx
|
||||
h+wPgx8LP2l/OvCZ/bnlS/yXZ4PzvpK+ln6z+tb4PfT7w6HsoSEJT8obkQI4bKCpqQBv9wPQEwEY
|
||||
NwEo00c19Ygho/8DRgj+F4/q7hFzAUwaAMT7AIQ3Aewe1iDLsNwYR2C+GB9AnZwU498mS3VyHM1F
|
||||
rcekScnQ0DtMP5KsAL51Dg0N1g8NfavEmr2P6ZgPo1p+RMeIMDmPFdPSba/ODoD/sn8BcJ0Q6U6Z
|
||||
H8IAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAASNQTFRF////
|
||||
hpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCb
|
||||
hpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCb
|
||||
hpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCb
|
||||
hpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCb
|
||||
hpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCbhpCb
|
||||
////f/Q5igAAAF90Uk5TAAAVQ26QsMjc7vMka6bhKITbEmwnmfcmpP2lFJX7AWPvZCC/XPEHmxjK
|
||||
GSw4PjfyLfAa4xwGnL5lE+2UmvZyc9cziSOn4kWxssvf4JFEF4o02CmWXckIIsCL5BZw3fSQaiOm
|
||||
AAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+QDHAczCg2f+2gAAAxo
|
||||
elRYdFJhdyBwcm9maWxlIHR5cGUgaWNjAABYha2Za64jKxKE/9cqZgk8kgSWAwkpzf43MB9lu9un
|
||||
7dbVlcanUdkUBUk+IoLq679m13/45Jz1CuezQtNYQw1qIcW7S5fuKjWVJFVSCqWVXkYKoW7hNl/C
|
||||
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
|
||||
NjsGcDG9WP066hbAzY/Sg9WvmZudAvg4vVd9W+8SwIrf/xs7GysdAriQXqv+zbcPYNnvf4+h1cXW
|
||||
AdxKb1UNt9oGsHY7PVU13F5rGcAn6aWq47hlAHfSQ1XHnXYB3E3vVC13WwVwLz1TtdxrFYB//2Fs
|
||||
LbUJ4H56peq53yKAufRI1TPXIoAH6ZGq50GLAD5Nj1Q9h80BTKQ3qqaJxgBOpSeqps8aA5hMT1RN
|
||||
k40BTKUnqqapxgA+T09UTQ8bAzhJT1RNJ40BfJGeqJq+bAzAvwc41rYaA7ianqiaZhsDSC9UXQYA
|
||||
ZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcA
|
||||
cAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAG
|
||||
AGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBnAHAGAGcAcAYAZwBwBgBn
|
||||
AHAGADeCAZT+pJ/yEjAAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgD
|
||||
gDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4Az
|
||||
ADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4
|
||||
A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOA
|
||||
MwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMA
|
||||
OAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgD
|
||||
gDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4Az
|
||||
ADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4
|
||||
A4BjB/D/S//w/sUADMAADMAADMAADMAADMAADMAADMAADMAADMAADMAADCA98N/SNzSAsPQNDSAs
|
||||
fUMDCEvf0ADC0jc0gLD0DQ0gLH1DAwhL39AAwtI3NICw9A0NICx9QwMIS9/QAMLSNzSAsPQNDSAs
|
||||
fUMDCEvf0ADC0jc0gLD0DQ0gLH1DAwhL39AAwtI3NICw9A0NICx9QwMIS9/QAMLSNzSAsPQNDSAs
|
||||
fUMDCEvf0ADC0jc0gLD0DQ0gLH1DAwhL39AAwtI3NICw9A0NICx9QwMIS9/QAMLSNzSAsPQNDSAs
|
||||
fUMDCEvf0ADC0jc0gLD0DQ0gLH1DAwhL39AAwtI3NICw9A0NICx9QwPQGDEAOAOAMwA4A4AzADgD
|
||||
gDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4Az
|
||||
ADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4
|
||||
A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzADgDgDMAOAOAMwA4A4AzALjGAK6mF6qm
|
||||
2cYAttITVdNWYwBfpCeqpi8bAzhJT1RNJ40BfJ6eqJoeNgYwlZ6omh41BjCZnqiaJhsDeJyeqJo+
|
||||
awxgIj1RNX3VGEA5TG9UPYelOYAH6ZGq50GLAObSI1XP2RYB3E+PVD33WwRQTqdXqpbTpU0AX6dn
|
||||
qpZ7rQK4m56pWp60CqDcSe9UHd+UdgEcp4eqjuOWAazdTi9VDbfXWgZQbqWnqoZbpW0Ai6vprerf
|
||||
6retAyjz6bHq33xpH8DKRnqt+rax3CGAsp6eq759V7oEcHMmvVf92rrZKYBylB6sfh2VbgFs7qQX
|
||||
q087mx0DKNt76c3qz9526RpA2U2PVn++L90DKDfSq9WXG2WQAC49Te9WP04uDRRAue4XhMfC4fUy
|
||||
WADl4Fp6u4Z37aAMGkDZn02v17Bm98vgAZQrFjDiZq+UYQIo+/7FkJG2tV+GC6AcLKTfoMEtHJRh
|
||||
AyjvXE6/QoO6/H4ZPoDyg58IjagbP5Q+Aihl198LjKC97593y4ECKD/+lH6Nuvrpx9JfAGXzyH8N
|
||||
jJSto83SZwClrK37PcGRsbG+9l93HDiAUpYv+G3xkbA6v/zfVxwigGcJnDuffpyanD+3+KIbDhVA
|
||||
KRPH/s/Rl9qd44kXX3DIAJ558vNS+pV6vqWfnzSeb/gAnjkzN+9XBV4yv8zPnWlzu14C+Nuvj397
|
||||
9PDp7zN/pF/O9sfM708fPvrt8a9t79YYgCRJkiRJkiRJkiRJGnl/AWNYpuT+rvp8AAAAJXRFWHRk
|
||||
YXRlOmNyZWF0ZQAyMDIwLTAzLTI4VDA3OjUxOjEwKzAwOjAwJ6kuaQAAACV0RVh0ZGF0ZTptb2Rp
|
||||
ZnkAMjAyMC0wMy0yOFQwNzo1MToxMCswMDowMFb0ltUAAAAodEVYdGljYzpjb3B5cmlnaHQAQ29w
|
||||
eXJpZ2h0IEFwcGxlIEluYy4sIDIwMjAKut6wAAAAF3RFWHRpY2M6ZGVzY3JpcHRpb24ARGlzcGxh
|
||||
eRcblbgAAAAASUVORK5CYII=" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><style>.cls-2{fill:#3c90ce;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-2" d="M256,496C123.67,496,16,388.33,16,256S123.67,16,256,16,496,123.67,496,256,388.33,496,256,496Zm0-470C129.17,26,26,129.18,26,256S129.17,486,256,486,486,382.82,486,256,382.83,26,256,26Z"/><path class="cls-2" d="M403.22,282l65.66-81.68A220.52,220.52,0,0,0,351.61,57.81Z"/><path class="cls-2" d="M476,256a220.86,220.86,0,0,0-4-41.5L334.74,385.3H434A219,219,0,0,0,476,256Z"/><path class="cls-2" d="M247.63,121.17,139.38,69.48A220,220,0,0,0,38.81,221Z"/><path class="cls-2" d="M140.75,184.81,37.07,234.35C36.38,241.48,36,248.69,36,256A219.13,219.13,0,0,0,91,401.4Z"/><path class="cls-2" d="M210.57,396.65l63.2,78.57a219.52,219.52,0,0,0,151.38-78.57Z"/><path class="cls-2" d="M124.21,307.38l-23.91,104A219.33,219.33,0,0,0,256,476c1.26,0,2.5-.07,3.76-.1Z"/><path class="cls-2" d="M364.52,164.42,338.67,52.12A220.21,220.21,0,0,0,151.16,62.54Z"/><path class="cls-2" d="M351.71,276.74c-.46-.11-1.12-.29-1.58-.37a45.28,45.28,0,0,0-5.2-.42,50.71,50.71,0,0,1-9.73-1.6,6.13,6.13,0,0,1-2.35-2.36l-2.19-.63a70.52,70.52,0,0,0-11.31-48.85l1.93-1.73a4.3,4.3,0,0,1,1-3.08,50.88,50.88,0,0,1,8.07-5.67,43.85,43.85,0,0,0,4.51-2.63c.35-.26.83-.67,1.2-1,2.6-2.08,3.2-5.66,1.33-8s-5.49-2.57-8.09-.49c-.38.29-.88.67-1.21,1a43.42,43.42,0,0,0-3.58,3.79,51.27,51.27,0,0,1-7.32,6.63,6.1,6.1,0,0,1-3.3.36l-2.06,1.47a71.09,71.09,0,0,0-45.06-21.77c-.05-.72-.11-2-.12-2.42a4.33,4.33,0,0,1-1.78-2.72,50.16,50.16,0,0,1,.62-9.84,45.29,45.29,0,0,0,.74-5.17c0-.43,0-1.07,0-1.54,0-3.33-2.43-6-5.43-6s-5.43,2.7-5.43,6c0,.05,0,.1,0,.15,0,.45,0,1,0,1.39a45.29,45.29,0,0,0,.74,5.17,50.87,50.87,0,0,1,.61,9.84,5.9,5.9,0,0,1-1.77,2.81l-.12,2.29a71.38,71.38,0,0,0-9.82,1.51,69.83,69.83,0,0,0-35.46,20.26c-.6-.41-1.65-1.16-2-1.39a4.32,4.32,0,0,1-3.23-.31,50.46,50.46,0,0,1-7.31-6.61,45,45,0,0,0-3.58-3.79c-.33-.29-.83-.67-1.2-1a6.42,6.42,0,0,0-3.78-1.42,5.22,5.22,0,0,0-4.33,1.91c-1.87,2.34-1.27,5.93,1.33,8l.09.06c.35.29.79.66,1.12.91a46.81,46.81,0,0,0,4.5,2.63,49.61,49.61,0,0,1,8.07,5.67,6,6,0,0,1,1.09,3.13l1.74,1.55a70.25,70.25,0,0,0-11.07,49l-2.28.66a7.61,7.61,0,0,1-2.33,2.36,51.5,51.5,0,0,1-9.73,1.6,45.38,45.38,0,0,0-5.21.41c-.41.08-1,.23-1.45.34h0l-.08,0c-3.2.78-5.26,3.72-4.6,6.61a5.75,5.75,0,0,0,7,4h.08l.1,0c.45-.1,1-.21,1.41-.32a45.65,45.65,0,0,0,4.87-1.87,51.49,51.49,0,0,1,9.46-2.78,6.07,6.07,0,0,1,3.12,1.1l2.37-.4A70.59,70.59,0,0,0,225,322.21l-1,2.37a5.46,5.46,0,0,1,.48,3.07,53.5,53.5,0,0,1-4.92,8.83,46,46,0,0,0-2.91,4.33c-.21.41-.48,1-.69,1.47a5.73,5.73,0,0,0,2.31,7.71c2.69,1.3,6-.07,7.49-3.06v0h0c.21-.42.5-1,.67-1.38a45,45,0,0,0,1.57-5c1.43-3.61,2.22-7.4,4.2-9.75a4.31,4.31,0,0,1,2.34-1.14l1.23-2.23a70.15,70.15,0,0,0,40.78,2.93,71.11,71.11,0,0,0,9.31-2.8l1.16,2.09a4.26,4.26,0,0,1,2.77,1.68,50.16,50.16,0,0,1,3.72,9.12,44.6,44.6,0,0,0,1.58,5c.18.4.47,1,.67,1.39,1.45,3,4.81,4.38,7.51,3.07s3.7-4.72,2.3-7.71c-.2-.43-.49-1.05-.7-1.46A45.8,45.8,0,0,0,302,336.4a51.18,51.18,0,0,1-4.82-8.62,4.27,4.27,0,0,1,.42-3.2,20.73,20.73,0,0,1-.9-2.19A70.66,70.66,0,0,0,328,283c.7.11,1.92.33,2.32.41a4.28,4.28,0,0,1,3-1.13,50.68,50.68,0,0,1,9.47,2.79,45.66,45.66,0,0,0,4.87,1.88c.39.1,1,.2,1.4.3l.11,0h.08a5.72,5.72,0,0,0,7-4C357,280.45,354.91,277.51,351.71,276.74Zm-51.57-55.29-23.2,16.45-.08,0a4.79,4.79,0,0,1-6.56-.88,4.71,4.71,0,0,1-1.05-2.77h0l-1.61-28.43A56.44,56.44,0,0,1,300.14,221.45Zm-43.7,31.16h8.73l5.43,6.79-2,8.46-7.84,3.77L253,267.85,251,259.39Zm-8.22-45.84a56.58,56.58,0,0,1,5.79-1l-1.61,28.47-.12.06a4.79,4.79,0,0,1-7.59,3.67l-.05,0-23.35-16.55A55.92,55.92,0,0,1,248.22,206.77Zm-35.39,25.3,21.32,19.07,0,.12a4.79,4.79,0,0,1-1.88,8.22l0,.09-27.32,7.89A55.83,55.83,0,0,1,212.83,232.07Zm28.54,50.65L230.51,309A56.27,56.27,0,0,1,208,280.73L236,276l0,.06A4.37,4.37,0,0,1,237,276a4.78,4.78,0,0,1,4.33,6.67Zm32,33.74a56.07,56.07,0,0,1-30.62-1.58L256.56,290h0a4.77,4.77,0,0,1,4-2.53A4.86,4.86,0,0,1,265,290h.1l13.82,25Q276.24,315.82,273.41,316.46Zm17.81-7.4-11-26.5,0-.05a4.78,4.78,0,0,1,2.32-6.2,4.65,4.65,0,0,1,1.83-.48,5.05,5.05,0,0,1,1.1.08l.05-.05,28.26,4.77A56,56,0,0,1,291.22,309.06Zm25.59-41.69-27.46-7.91,0-.12a4.79,4.79,0,0,1-3.4-5.68,4.7,4.7,0,0,1,1.52-2.54v-.06l21.2-19a56.92,56.92,0,0,1,8.17,35.28Z"/></g></g></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 4.2 KiB |
@ -117,12 +117,11 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
try {
|
||||
await Promise.all(stores.map(store => store.loadAll()));
|
||||
const subscriptions = stores.map(store => store.subscribe());
|
||||
|
||||
await when(() => this.isUnmounting);
|
||||
subscriptions.forEach(dispose => dispose()); // unsubscribe all
|
||||
} catch(error) {
|
||||
console.log("catched", error)
|
||||
}
|
||||
await when(() => this.isUnmounting);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
||||
@ -226,7 +226,7 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isHidden, subMenus = [], icon, text, url, children, className } = this.props;
|
||||
const { id, isHidden, subMenus = [], icon, text, url, children, className } = this.props;
|
||||
if (isHidden) {
|
||||
return null;
|
||||
}
|
||||
@ -234,7 +234,7 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
|
||||
if (extendedView) {
|
||||
const isActive = this.isActive();
|
||||
return (
|
||||
<div className={cssNames("SidebarNavItem", className)}>
|
||||
<div id={id} className={cssNames("SidebarNavItem", className)}>
|
||||
<div className={cssNames("nav-item", { active: isActive })} onClick={this.toggleSubMenu}>
|
||||
{icon}
|
||||
<span className="link-text">{text}</span>
|
||||
|
||||
0
src/renderer/utils/__tests__/arrays.test.ts
Normal file
0
src/renderer/utils/arrays.ts
Normal file
@ -2,15 +2,25 @@
|
||||
|
||||
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where your might need to do something to ensure the application works smoothly. So please read through the release highlights!
|
||||
|
||||
## 3.5.0-beta.1 (current version)
|
||||
## 3.5.0 (current version)
|
||||
|
||||
- Dynamic dashboard UI based on RBAC rules
|
||||
- Dynamic dashboard UI based on RBAC rules (hides non-accessible menus)
|
||||
- Show object reference for all objects
|
||||
- Unify scrollbars/paddings
|
||||
- New logo
|
||||
- Remove Helm release update checker
|
||||
- Improve Helm release version detection
|
||||
- Show owner reference on all resource details
|
||||
- Fix: add arch node selector for hybrid clusters
|
||||
- Fix pod shell command on Windows
|
||||
- Fix app freeze after closing terminal on Windows
|
||||
- Fix: use correct kubeconfig context on terminal when switching cluster
|
||||
- Fix error when closing Lens on Windows
|
||||
- Fix: deploy kube-state-metrics component only to amd64 nodes
|
||||
- Translation correction: transit to transmit
|
||||
- Remove Kontena reference from Lens logo
|
||||
- Track telemetry pref changed event
|
||||
- Integration tests using spectron
|
||||
|
||||
## 3.4.0
|
||||
|
||||
@ -280,4 +290,3 @@ Here you can find description of changes we've built into each release. While we
|
||||
## 2.0.0
|
||||
|
||||
Initial release of the Lens desktop application. Basic functionality with auto-import of users local kubeconfig for cluster access.
|
||||
|
||||
|
||||