diff --git a/Makefile b/Makefile index b22527daec..9fdb588215 100644 --- a/Makefile +++ b/Makefile @@ -39,10 +39,6 @@ dev: binaries/client build-extensions static/build/LensDev.html lint: yarn lint -.PHONY: release-version -release-version: - npm version $(CMD_ARGS) --git-tag-version false - .PHONY: tag-release tag-release: scripts/tag-release.sh $(CMD_ARGS) diff --git a/RELEASE_GUIDE.md b/RELEASE_GUIDE.md index b11a47c134..a2f7a6db32 100644 --- a/RELEASE_GUIDE.md +++ b/RELEASE_GUIDE.md @@ -2,15 +2,17 @@ Lens releases are built by CICD automatically on git tags. The typical release process flow is the following: +1. It is recommended to perform the release process from a folder used solely meant for creating releases (i.e. not your dev folder), with the Lens repo initialized (origin set to https://github.com/lensapp/lens). + 1. If doing a patch release checkout the `release/vMAJOR.MINOR` branch for the appropriate `MAJOR`/`MINOR` version and manually `cherry-pick` the PRs required for the patch that were commited to master. If there are any conflicts they must be resolved manually. If necessary, get assistance from the PR authors. -1. From a clean and up to date `master` (or `release/vMAJOR.MINOR` if doing a patch release) run `make release-version ` where `` is one of the following: +1. From a clean and up to date `master` (or `release/vMAJOR.MINOR` if doing a patch release) run `npm version --git-tag-version false` where `` is one of the following: - `major` - `minor` - `patch` - - `premajor` - - `preminor` - - `prepatch` + - `premajor` [--preid=] + - `preminor` [--preid=] + - `prepatch` [--preid=] - `prerelease [--preid=]` where `` is generally one of: @@ -18,7 +20,7 @@ Lens releases are built by CICD automatically on git tags. The typical release p - `beta` - `rc` - This assumes origin is set to https://github.com/lensapp/lens.git. If not then set GIT_REMOTE to the remote that is set to https://github.com/lensapp/lens.git. For example run `GIT_REMOTE=upstream make release-version ...` + This assumes origin is set to https://github.com/lensapp/lens.git. If not then set GIT_REMOTE to the remote that is set to https://github.com/lensapp/lens.git. For example run `GIT_REMOTE=upstream npm version ...` 1. Open the PR (git should have printed a link to GitHub in the console) with the contents of all the accepted PRs since the last release. The PR description needs to be filled with the draft release description. From https://github.com/lensapp/lens click on Releases, the draft release should be first in the list, click `Edit` and copy/paste the markdown to the PR description. Add the `skip-changelog` label and click `Create Pull Request`. If this is a patch release be sure to set the PR base branch to `release/vMAJOR.MINOR` instead of `master`. 1. After the PR is accepted and passes CI (and before merging), go to the same branch and run `make tag-release` (set GIT_REMOTE if necessary). This additionally triggers the azure jobs to build the binaries and put them on S3. 1. If the CI fails at this stage the problem needs to be fixed. Sometimes an azure job fails due to outside service issues (e.g. Apple signing occasionally fails), in which case the specific azure job can be rerun from https://dev.azure.com/lensapp/lensapp/_build. Otherwise changes to the codebase may need to be done and committed to the release branch and pushed to https://github.com/lensapp/lens. CI will run again. As well the release tag needs to be manually set to this new commit. You can do something like: diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts index e7b564d888..7135c93e1f 100644 --- a/integration/__tests__/cluster-pages.tests.ts +++ b/integration/__tests__/cluster-pages.tests.ts @@ -36,7 +36,7 @@ function getSidebarSelectors(itemId: string) { return { expandSubMenu: `${root} .nav-item`, - subMenuLink: (href: string) => `[data-testid=cluster-sidebar] .sub-menu a[href^="/${href}"]`, + subMenuLink: (href: string) => `[data-testid=cluster-sidebar] .sub-menu a[href^="${href}"]`, }; } @@ -73,161 +73,169 @@ function isTopPageTest(test: CommonPageTest): test is TopPageTest { const commonPageTests: CommonPageTest[] = [{ page: { name: "Cluster", - href: "cluster", - expectedSelector: "div.ClusterOverview div.label", + href: "/overview", + expectedSelector: "div[data-testid='cluster-overview-page'] div.label", expectedText: "CPU" } }, { page: { name: "Nodes", - href: "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" - }] + 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" - }] + 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" - }] + 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" - }] + 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", + href: "/namespaces", expectedSelector: "h5.title", expectedText: "Namespaces" } @@ -235,72 +243,78 @@ const commonPageTests: CommonPageTest[] = [{ { page: { name: "Events", - href: "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" - }] + 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" - }] + 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" - }] + pages: [ + { + name: "Definitions", + href: "/crd/definitions", + expectedSelector: "h5.title", + expectedText: "Custom Resources" + }, + ] }]; utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { @@ -321,7 +335,7 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { for (const test of commonPageTests) { if (isTopPageTest(test)) { const { href, expectedText, expectedSelector } = test.page; - const menuButton = await frame.waitForSelector(`a[href^="/${href}"]`); + const menuButton = await frame.waitForSelector(`a[href^="${href}"]`); await menuButton.click(); await frame.waitForSelector(`${expectedSelector} >> text='${expectedText}'`); diff --git a/package.json b/package.json index 6fa107cdb0..473172ac3f 100644 --- a/package.json +++ b/package.json @@ -212,7 +212,7 @@ "jsonpath": "^1.1.1", "lodash": "^4.17.15", "mac-ca": "^1.0.6", - "marked": "^2.0.3", + "marked": "^2.1.3", "md5-file": "^5.0.0", "mobx": "^6.3.0", "mobx-observable-history": "^2.0.1", @@ -222,7 +222,7 @@ "moment-timezone": "^0.5.33", "monaco-editor": "^0.29.1", "monaco-editor-webpack-plugin": "^5.0.0", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.5", "node-pty": "^0.10.1", "npm": "^6.14.15", "p-limit": "^3.1.0", @@ -253,7 +253,6 @@ "ws": "^7.5.5" }, "devDependencies": { - "@emeraldpay/hashicon-react": "^0.4.0", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", @@ -263,7 +262,7 @@ "@testing-library/dom": "^8.9.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.6", - "@testing-library/user-event": "^13.2.1", + "@testing-library/user-event": "^13.5.0", "@types/byline": "^4.2.33", "@types/chart.js": "^2.9.34", "@types/color": "^3.0.2", @@ -286,18 +285,18 @@ "@types/mini-css-extract-plugin": "^0.9.1", "@types/mock-fs": "^4.13.1", "@types/module-alias": "^2.0.1", - "@types/node": "14.17.26", + "@types/node": "14.17.27", "@types/node-fetch": "^2.5.12", "@types/npm": "^2.0.32", "@types/progress-bar-webpack-plugin": "^2.1.2", "@types/proper-lockfile": "^4.1.2", "@types/randomcolor": "^0.5.6", "@types/react": "^17.0.29", - "@types/react-beautiful-dnd": "^13.1.1", + "@types/react-beautiful-dnd": "^13.1.2", "@types/react-dom": "^17.0.9", "@types/react-router-dom": "^5.3.1", "@types/react-select": "3.1.2", - "@types/react-table": "^7.7.6", + "@types/react-table": "^7.7.7", "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", "@types/readable-stream": "^2.3.11", @@ -315,7 +314,7 @@ "@types/webdriverio": "^4.13.0", "@types/webpack": "^4.41.31", "@types/webpack-dev-server": "^3.11.6", - "@types/webpack-env": "^1.16.2", + "@types/webpack-env": "^1.16.3", "@types/webpack-node-externals": "^1.7.1", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.29.1", @@ -330,13 +329,13 @@ "electron": "^13.5.1", "electron-builder": "^22.11.11", "electron-notarize": "^0.3.0", - "esbuild": "^0.12.24", - "esbuild-loader": "^2.15.1", + "esbuild": "^0.13.8", + "esbuild-loader": "^2.16.0", "eslint": "^7.32.0", "eslint-plugin-header": "^3.1.1", "eslint-plugin-react": "^7.26.1", "eslint-plugin-react-hooks": "^4.2.0", - "eslint-plugin-unused-imports": "^1.0.1", + "eslint-plugin-unused-imports": "^1.1.5", "file-loader": "^6.2.0", "flex.box": "^3.4.4", "fork-ts-checker-webpack-plugin": "^5.2.1", @@ -352,7 +351,7 @@ "mini-css-extract-plugin": "^1.6.2", "node-gyp": "7.1.2", "node-loader": "^1.0.3", - "nodemon": "^2.0.13", + "nodemon": "^2.0.14", "playwright": "^1.15.2", "postcss": "^8.3.6", "postcss-loader": "4.3.0", @@ -374,7 +373,7 @@ "tailwindcss": "^2.2.17", "ts-jest": "26.5.6", "ts-loader": "^7.0.5", - "ts-node": "^10.2.1", + "ts-node": "^10.3.0", "type-fest": "^1.0.2", "typed-emitter": "^1.3.1", "typedoc": "0.22.6", diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 31e0251966..8a3c3e0858 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -23,10 +23,11 @@ import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc"; import { ClusterStore } from "../cluster-store"; -import { requestMain } from "../ipc"; +import { broadcastMessage, requestMain } from "../ipc"; import { CatalogCategory, CatalogCategorySpec } from "../catalog"; import { app } from "electron"; import type { CatalogEntitySpec } from "../catalog/catalog-entity"; +import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; export interface KubernetesClusterPrometheusMetrics { address?: { @@ -114,7 +115,10 @@ export class KubernetesCluster extends CatalogEntity context.navigate(`/entity/${this.metadata.uid}/settings`) + onClick: () => broadcastMessage( + IpcRendererNavigationEvents.NAVIGATE_IN_APP, + `/entity/${this.metadata.uid}/settings`, + ), }); } diff --git a/src/common/catalog/catalog-entity.ts b/src/common/catalog/catalog-entity.ts index cbe59bae57..9d549f89de 100644 --- a/src/common/catalog/catalog-entity.ts +++ b/src/common/catalog/catalog-entity.ts @@ -172,7 +172,10 @@ export interface CatalogEntitySettingsMenu { } export interface CatalogEntityContextMenuContext { - navigate: (url: string) => void; + /** + * Navigate to the specified pathname + */ + navigate: (pathname: string) => void; menuItems: CatalogEntityContextMenu[]; } diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 8e8940c644..26806ae239 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -40,7 +40,6 @@ export class ClusterStore extends BaseStore { private static StateChannel = "cluster:state"; clusters = observable.map(); - removedClusters = observable.map(); protected disposer = disposer(); @@ -144,7 +143,6 @@ export class ClusterStore extends BaseStore { protected fromStore({ clusters = [] }: ClusterStoreModel = {}) { const currentClusters = new Map(this.clusters); const newClusters = new Map(); - const removedClusters = new Map(); // update new clusters for (const clusterModel of clusters) { @@ -162,15 +160,7 @@ export class ClusterStore extends BaseStore { } } - // update removed clusters - currentClusters.forEach(cluster => { - if (!newClusters.has(cluster.id)) { - removedClusters.set(cluster.id, cluster); - } - }); - this.clusters.replace(newClusters); - this.removedClusters.replace(removedClusters); } toJSON(): ClusterStoreModel { diff --git a/src/common/k8s-api/endpoints/secret.api.ts b/src/common/k8s-api/endpoints/secret.api.ts index 99200933ff..7c90c2e9c9 100644 --- a/src/common/k8s-api/endpoints/secret.api.ts +++ b/src/common/k8s-api/endpoints/secret.api.ts @@ -41,12 +41,9 @@ export interface ISecretRef { name: string; } -export interface Secret { +export interface SecretData extends KubeJsonApiData { type: SecretType; - data: { - [prop: string]: string; - token?: string; - }; + data?: Record; } export class Secret extends KubeObject { @@ -54,7 +51,10 @@ export class Secret extends KubeObject { static namespaced = true; static apiBase = "/api/v1/secrets"; - constructor(data: KubeJsonApiData) { + declare type: SecretType; + declare data: Record; + + constructor(data: SecretData) { super(data); autoBind(this); diff --git a/src/common/k8s-api/json-api.ts b/src/common/k8s-api/json-api.ts index dde8f5f5a2..15bad9864a 100644 --- a/src/common/k8s-api/json-api.ts +++ b/src/common/k8s-api/json-api.ts @@ -52,6 +52,7 @@ export interface JsonApiConfig { apiBase: string; serverAddress: string; debug?: boolean; + getRequestOptions?: () => Promise; } export class JsonApi { static reqInitDefault: RequestInit = { @@ -68,18 +69,26 @@ export class JsonApi { this.config = Object.assign({}, JsonApi.configDefault, config); this.reqInit = merge({}, JsonApi.reqInitDefault, reqInit); this.parseResponse = this.parseResponse.bind(this); + this.getRequestOptions = config.getRequestOptions ?? (() => Promise.resolve({})); } public onData = new EventEmitter<[D, Response]>(); public onError = new EventEmitter<[JsonApiErrorParsed, Response]>(); + + private getRequestOptions: JsonApiConfig["getRequestOptions"]; get(path: string, params?: P, reqInit: RequestInit = {}) { return this.request(path, params, { ...reqInit, method: "get" }); } - getResponse(path: string, params?: P, init: RequestInit = {}): Promise { + async getResponse(path: string, params?: P, init: RequestInit = {}): Promise { let reqUrl = `${this.config.serverAddress}${this.config.apiBase}${path}`; - const reqInit: RequestInit = merge({}, this.reqInit, init); + const reqInit: RequestInit = merge( + {}, + this.reqInit, + await this.getRequestOptions(), + init + ); const { query } = params || {} as P; if (!reqInit.method) { @@ -113,7 +122,12 @@ export class JsonApi { protected async request(path: string, params?: P, init: RequestInit = {}) { let reqUrl = `${this.config.serverAddress}${this.config.apiBase}${path}`; - const reqInit: RequestInit = merge({}, this.reqInit, init); + const reqInit: RequestInit = merge( + {}, + this.reqInit, + await this.getRequestOptions(), + init + ); const { data, query } = params || {} as P; if (data && !reqInit.body) { diff --git a/src/common/k8s-api/kube-api.ts b/src/common/k8s-api/kube-api.ts index 54b0e5726f..7e29500689 100644 --- a/src/common/k8s-api/kube-api.ts +++ b/src/common/k8s-api/kube-api.ts @@ -22,6 +22,7 @@ // Base class for building all kubernetes apis import merge from "lodash/merge"; +import { isFunction } from "lodash"; import { stringify } from "querystring"; import { apiKubePrefix, isDevelopment } from "../../common/vars"; import logger from "../../main/logger"; @@ -108,7 +109,7 @@ export interface IRemoteKubeApiConfig { skipTLSVerify?: boolean; } user: { - token?: string; + token?: string | (() => Promise); clientCertificateData?: string; clientKeyData?: string; } @@ -135,12 +136,6 @@ export function forCluster(cluster: ILocalKubeApiConfig, k export function forRemoteCluster(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor): KubeApi { const reqInit: RequestInit = {}; - if (config.user.token) { - reqInit.headers = { - "Authorization": `Bearer ${config.user.token}` - }; - } - const agentOptions: AgentOptions = {}; if (config.cluster.skipTLSVerify === true) { @@ -163,10 +158,18 @@ export function forRemoteCluster(config: IRemoteKubeApiCon reqInit.agent = new Agent(agentOptions); } + const token = config.user.token; const request = new KubeJsonApi({ serverAddress: config.cluster.server, apiBase: "", debug: isDevelopment, + ...(token ? { + getRequestOptions: async () => ({ + headers: { + "Authorization": `Bearer ${isFunction(token) ? await token() : token}` + } + }) + } : {}) }, reqInit); return new KubeApi({ diff --git a/src/common/k8s-api/kube-object.store.ts b/src/common/k8s-api/kube-object.store.ts index 6fd1209d4e..e8b13d8cd7 100644 --- a/src/common/k8s-api/kube-object.store.ts +++ b/src/common/k8s-api/kube-object.store.ts @@ -26,7 +26,6 @@ import { autoBind, noop, rejectPromiseBy } from "../utils"; import { KubeObject, KubeStatus } from "./kube-object"; import type { IKubeWatchEvent } from "./kube-watch-api"; import { ItemStore } from "../item.store"; -import { apiManager } from "./api-manager"; import { ensureObjectSelfLink, IKubeApiQueryParams, KubeApi } from "./kube-api"; import { parseKubeApi } from "./kube-api-parse"; import type { KubeJsonApiData } from "./kube-json-api"; @@ -402,12 +401,11 @@ export abstract class KubeObjectStore extends ItemStore for (const { type, object } of this.eventsBuffer.clear()) { const index = items.findIndex(item => item.getId() === object.metadata?.uid); const item = items[index]; - const api = apiManager.getApiByKind(object.kind, object.apiVersion); switch (type) { case "ADDED": case "MODIFIED": - const newItem = new api.objectConstructor(object) as T; + const newItem = new this.api.objectConstructor(object); if (!item) { items.push(newItem); diff --git a/src/common/routes/cluster.ts b/src/common/routes/cluster.ts index 3a39ccd22a..2ba39ae53f 100644 --- a/src/common/routes/cluster.ts +++ b/src/common/routes/cluster.ts @@ -23,7 +23,7 @@ import type { RouteProps } from "react-router"; import { buildURL } from "../utils/buildUrl"; export const clusterRoute: RouteProps = { - path: "/cluster" + path: "/overview" }; export const clusterURL = buildURL(clusterRoute.path); diff --git a/src/common/routes/port-forwards.ts b/src/common/routes/port-forwards.ts index d05b8583ab..bf644e5003 100644 --- a/src/common/routes/port-forwards.ts +++ b/src/common/routes/port-forwards.ts @@ -23,10 +23,11 @@ import type { RouteProps } from "react-router"; import { buildURL } from "../utils/buildUrl"; export const portForwardsRoute: RouteProps = { - path: "/port-forwards" + path: "/port-forwards/:forwardport?" }; export interface PortForwardsRouteParams { + forwardport?: string; } export const portForwardsURL = buildURL(portForwardsRoute.path); diff --git a/src/common/user-store/preferences-helpers.ts b/src/common/user-store/preferences-helpers.ts index 6fdad83c9d..a597d65955 100644 --- a/src/common/user-store/preferences-helpers.ts +++ b/src/common/user-store/preferences-helpers.ts @@ -135,12 +135,32 @@ const allowErrorReporting: PreferenceDescription = { }, }; +export interface DownloadMirror { + url: string; + label: string; + platforms: Set; +} + +export const defaultPackageMirror = "default"; +export const packageMirrors = new Map([ + [defaultPackageMirror, { + url: "https://storage.googleapis.com/kubernetes-release/release", + label: "Default (Google)", + platforms: new Set(["darwin", "win32", "linux"]), + }], + ["china", { + url: "https://mirror.azure.cn/kubernetes/kubectl", + label: "China (Azure)", + platforms: new Set(["win32", "linux"]), + }], +]); + const downloadMirror: PreferenceDescription = { fromStore(val) { - return val ?? "default"; + return packageMirrors.has(val) ? val : defaultPackageMirror; }, toStore(val) { - if (!val || val === "default") { + if (!val || val === defaultPackageMirror) { return undefined; } diff --git a/src/common/utils/array.ts b/src/common/utils/array.ts new file mode 100644 index 0000000000..b0561e3eaf --- /dev/null +++ b/src/common/utils/array.ts @@ -0,0 +1,44 @@ +/** + * 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 type Tuple = N extends N ? number extends N ? T[] : _TupleOf : never; +type _TupleOf = R["length"] extends N ? R : _TupleOf; + +/** + * + * @param sources The source arrays + * @yields A tuple of the next element from each of the sources + * @returns The tuple of all the sources as soon as at least one of the sources is exausted + */ +export function* zipStrict(...sources: Tuple): Iterator, Tuple> { + const maxSafeLength = sources.reduce((prev, cur) => Math.min(prev, cur.length), Number.POSITIVE_INFINITY); + + if (!isFinite(maxSafeLength)) { + // There are no sources, thus just return + return [] as Tuple; + } + + for (let i = 0; i < maxSafeLength; i += 1) { + yield sources.map(source => source[i]) as Tuple; + } + + return sources.map(source => source.slice(maxSafeLength)) as Tuple; +} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 6035b9f408..fdf91d39c1 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -60,5 +60,6 @@ export * from "./convertMemory"; export * from "./convertCpu"; import * as iter from "./iter"; +import * as array from "./array"; -export { iter }; +export { iter, array }; diff --git a/src/common/utils/iter.ts b/src/common/utils/iter.ts index dd97c8feef..6271a05969 100644 --- a/src/common/utils/iter.ts +++ b/src/common/utils/iter.ts @@ -185,3 +185,19 @@ export function reduce(src: Iterable, reducer: (acc: R, cur: T) => export function join(src: Iterable, connector = ","): string { return reduce(src, (acc, cur) => `${acc}${connector}${cur}`, ""); } + +/** + * Iterate through `src` and return `true` if `fn` returns a thruthy value for every yielded value. + * Otherwise, return `false`. This function short circuits. + * @param src The type to be iterated over + * @param fn A function to check each iteration + */ +export function every(src: Iterable, fn: (val: T) => any): boolean { + for (const val of src) { + if (!fn(val)) { + return false; + } + } + + return true; +} diff --git a/src/common/utils/sort-compare.ts b/src/common/utils/sort-compare.ts index 041a239bb5..7167761151 100644 --- a/src/common/utils/sort-compare.ts +++ b/src/common/utils/sort-compare.ts @@ -24,16 +24,44 @@ import * as iter from "./iter"; import type { RawHelmChart } from "../k8s-api/endpoints/helm-charts.api"; import logger from "../logger"; -export function sortCompare(left: T, right: T): -1 | 0 | 1 { +export enum Ordering { + LESS = -1, + EQUAL = 0, + GREATER = 1, +} + +/** + * This function switches the direction of `ordering` if `direction` is `"desc"` + * @param ordering The original ordering (assumed to be an "asc" ordering) + * @param direction The new desired direction + */ +export function rectifyOrdering(ordering: Ordering, direction: "asc" | "desc"): Ordering { + if (direction === "desc") { + return -ordering; + } + + return ordering; +} + +/** + * An ascending sorting function + * @param left An item from an array + * @param right An item from an array + * @returns The relative ordering in an ascending manner. + * - Less if left < right + * - Equal if left == right + * - Greater if left > right + */ +export function sortCompare(left: T, right: T): Ordering { if (left < right) { - return -1; + return Ordering.LESS; } if (left === right) { - return 0; + return Ordering.EQUAL; } - return 1; + return Ordering.GREATER; } interface ChartVersion { @@ -41,17 +69,17 @@ interface ChartVersion { __version?: SemVer; } -export function sortCompareChartVersions(left: ChartVersion, right: ChartVersion): -1 | 0 | 1 { +export function sortCompareChartVersions(left: ChartVersion, right: ChartVersion): Ordering { if (left.__version && right.__version) { return semver.compare(right.__version, left.__version); } if (!left.__version && right.__version) { - return 1; + return Ordering.GREATER; } if (left.__version && !right.__version) { - return -1; + return Ordering.LESS; } return sortCompare(left.version, right.version); @@ -83,5 +111,5 @@ export function sortCharts(charts: RawHelmChart[]) { return chartsWithVersion .sort(sortCompareChartVersions) - .map(chart => (delete chart.__version, chart)); + .map(chart => (delete chart.__version, chart as RawHelmChart)); } diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 8cd6664b6a..c082bc78c9 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -21,7 +21,7 @@ import "../common/cluster-ipc"; import type http from "http"; -import { action, autorun, makeObservable, observable, observe, reaction, toJS } from "mobx"; +import { action, makeObservable, observable, observe, reaction, toJS } from "mobx"; import { Cluster } from "./cluster"; import logger from "./logger"; import { apiKubePrefix } from "../common/vars"; @@ -71,21 +71,6 @@ export class ClusterManager extends Singleton { } }); - // auto-stop removed clusters - autorun(() => { - const removedClusters = Array.from(this.store.removedClusters.values()); - - if (removedClusters.length > 0) { - const meta = removedClusters.map(cluster => cluster.getMeta()); - - logger.info(`${logPrefix} removing clusters`, meta); - removedClusters.forEach(cluster => cluster.disconnect()); - this.store.removedClusters.clear(); - } - }, { - delay: 250 - }); - ipcMainOn("network:offline", this.onNetworkOffline); ipcMainOn("network:online", this.onNetworkOnline); }); diff --git a/src/main/helm/helm-chart-manager.ts b/src/main/helm/helm-chart-manager.ts index e99554dcd0..b7afb6ff3d 100644 --- a/src/main/helm/helm-chart-manager.ts +++ b/src/main/helm/helm-chart-manager.ts @@ -24,10 +24,15 @@ import v8 from "v8"; import * as yaml from "js-yaml"; import type { HelmRepo } from "./helm-repo-manager"; import logger from "../logger"; -import { promiseExec } from "../promise-exec"; +import { promiseExecFile } from "../promise-exec"; import { helmCli } from "./helm-cli"; import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api"; -import { sortCharts } from "../../common/utils"; +import { iter, sortCharts } from "../../common/utils"; + +interface ChartCacheEntry { + data: Buffer, + mtimeMs: number, +} export interface HelmCacheFile { apiVersion: string; @@ -35,7 +40,7 @@ export interface HelmCacheFile { } export class HelmChartManager { - static #cache = new Map(); + static #cache = new Map(); private constructor(protected repo: HelmRepo) {} @@ -59,16 +64,17 @@ export class HelmChartManager { } } - private async executeCommand(action: string, name: string, version?: string) { + private async executeCommand(args: string[], name: string, version?: string) { const helm = await helmCli.binaryPath(); - const cmd = [`"${helm}" ${action} ${this.repo.name}/${name}`]; + + args.push(`${this.repo.name}/${name}`); if (version) { - cmd.push("--version", version); + args.push("--version", version); } try { - const { stdout } = await promiseExec(cmd.join(" ")); + const { stdout } = await promiseExecFile(helm, args); return stdout; } catch (error) { @@ -77,47 +83,69 @@ export class HelmChartManager { } public async getReadme(name: string, version?: string) { - return this.executeCommand("show readme", name, version); + return this.executeCommand(["show", "readme"], name, version); } public async getValues(name: string, version?: string) { - return this.executeCommand("show values", name, version); + return this.executeCommand(["show", "values"], name, version); + } + + protected async updateYamlCache() { + const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8"); + const cacheFileStats = await fs.promises.stat(this.repo.cacheFilePath); + const data = yaml.load(cacheFile) as string | number | HelmCacheFile; + + if (!data || typeof data !== "object" || typeof data.entries !== "object") { + throw Object.assign(new TypeError("Helm Cache file does not parse correctly"), { file: this.repo.cacheFilePath, data }); + } + + const normalized = normalizeHelmCharts(this.repo.name, data.entries); + + HelmChartManager.#cache.set(this.repo.name, { + data: v8.serialize(normalized), + mtimeMs: cacheFileStats.mtimeMs, + }); } protected async cachedYaml(): Promise { if (!HelmChartManager.#cache.has(this.repo.name)) { - const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8"); - const data = yaml.load(cacheFile) as string | number | HelmCacheFile; + await this.updateYamlCache(); + } else { + const newStats = await fs.promises.stat(this.repo.cacheFilePath); + const cacheEntry = HelmChartManager.#cache.get(this.repo.name); - if (typeof data !== "object" || !data) { - return {}; + if (cacheEntry.mtimeMs < newStats.mtimeMs) { + await this.updateYamlCache(); } - - /** - * Do some initial preprocessing on the data, so as to avoid needing to do it later - * 1. Set the repo name - * 2. Normalize the created date - * 3. Filter out deprecated items - */ - - const normalized = Object.fromEntries( - Object.entries(data.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 v8.deserialize(HelmChartManager.#cache.get(this.repo.name)); + return v8.deserialize(HelmChartManager.#cache.get(this.repo.name).data); } } + +/** + * Do some initial preprocessing on the data, so as to avoid needing to do it later + * 1. Set the repo name + * 2. Normalize the created date + * 3. Filter out charts that only have deprecated entries + */ +function normalizeHelmCharts(repoName: string, entries: RepoHelmChartList): RepoHelmChartList { + return Object.fromEntries( + iter.filter( + iter.map( + Object.entries(entries), + ([name, charts]) => [ + name, + sortCharts( + charts.map(chart => ({ + ...chart, + created: Date.parse(chart.created).toString(), + repo: repoName, + })), + ), + ] as const + ), + ([, charts]) => !charts.every(chart => chart.deprecated), + ) + ); +} diff --git a/src/main/index.ts b/src/main/index.ts index 3dd478fc76..18bfa0980e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -27,7 +27,7 @@ import * as Mobx from "mobx"; import * as LensExtensionsCommonApi from "../extensions/common-api"; import * as LensExtensionsMainApi from "../extensions/main-api"; import { app, autoUpdater, dialog, powerMonitor } from "electron"; -import { appName, isIntegrationTesting, isMac, productName } from "../common/vars"; +import { appName, isIntegrationTesting, isMac, isWindows, productName } from "../common/vars"; import { LensProxy } from "./lens-proxy"; import { WindowManager } from "./window-manager"; import { ClusterManager } from "./cluster-manager"; @@ -133,9 +133,7 @@ app.on("ready", async () => { bindBroadcastHandlers(); - powerMonitor.on("shutdown", () => { - app.exit(); - }); + powerMonitor.on("shutdown", () => app.exit()); registerFileProtocol("static", __static); @@ -183,7 +181,8 @@ app.on("ready", async () => { await lensProxy.listen(); } catch (error) { dialog.showErrorBox("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`); - app.exit(); + + return app.exit(); } // test proxy connection @@ -193,13 +192,27 @@ app.on("ready", async () => { if (getAppVersion() !== versionFromProxy) { logger.error("Proxy server responded with invalid response"); - app.exit(); - } else { - logger.info("⚡ LensProxy connection OK"); + + return app.exit(); } + + logger.info("⚡ LensProxy connection OK"); } catch (error) { logger.error(`🛑 LensProxy: failed connection test: ${error}`); - app.exit(); + + const hostsPath = isWindows + ? "C:\\windows\\system32\\drivers\\etc\\hosts" + : "/etc/hosts"; + const message = [ + `Failed connection test: ${error}`, + "Check to make sure that no other versions of Lens are running", + `Check ${hostsPath} to make sure that it is clean and that the localhost loopback is at the top and set to 127.0.0.1`, + "If you have HTTP_PROXY or http_proxy set in your environment, make sure that the localhost and the ipv4 loopback address 127.0.0.1 are added to the NO_PROXY environment variable.", + ]; + + dialog.showErrorBox("Lens Proxy Error", message.join("\n\n")); + + return app.exit(); } initializers.initRegistries(); diff --git a/src/main/initializers/ipc.ts b/src/main/initializers/ipc.ts index 0eb36b4f3a..c16c3e1fe1 100644 --- a/src/main/initializers/ipc.ts +++ b/src/main/initializers/ipc.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { BrowserWindow, dialog, IpcMainInvokeEvent } from "electron"; +import { app, BrowserWindow, dialog, IpcMainInvokeEvent } from "electron"; import { KubernetesCluster } from "../../common/catalog-entities"; import { clusterFrameMap } from "../../common/cluster-frames"; import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../common/cluster-ipc"; @@ -32,6 +32,8 @@ import { pushCatalogToRenderer } from "../catalog-pusher"; import { ClusterManager } from "../cluster-manager"; import { ResourceApplier } from "../resource-applier"; import { WindowManager } from "../window-manager"; +import path from "path"; +import { remove } from "fs-extra"; export function initIpcMainHandlers() { ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { @@ -79,9 +81,11 @@ export function initIpcMainHandlers() { } }); - ipcMainHandle(clusterDeleteHandler, (event, clusterId: ClusterId) => { + ipcMainHandle(clusterDeleteHandler, async (event, clusterId: ClusterId) => { appEventBus.emit({ name: "cluster", action: "remove" }); - const cluster = ClusterStore.getInstance().getById(clusterId); + + const clusterStore = ClusterStore.getInstance(); + const cluster = clusterStore.getById(clusterId); if (!cluster) { return; @@ -89,6 +93,16 @@ export function initIpcMainHandlers() { cluster.disconnect(); clusterFrameMap.delete(cluster.id); + + // Remove from the cluster store as well, this should clear any old settings + clusterStore.clusters.delete(cluster.id); + + try { + // remove the local storage file + const localStorageFilePath = path.resolve(app.getPath("userData"), "lens-local-storage", `${cluster.id}.json`); + + await remove(localStorageFilePath); + } catch {} }); ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => { diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 4695e8272e..846133d828 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -32,6 +32,7 @@ import { getBundledKubectlVersion } from "../common/utils/app-version"; import { isDevelopment, isWindows, isTestEnv } from "../common/vars"; import { SemVer } from "semver"; import { getPath } from "../common/utils/getPath"; +import { defaultPackageMirror, packageMirrors } from "../common/user-store/preferences-helpers"; const bundledVersion = getBundledKubectlVersion(); const kubectlMap: Map = new Map([ @@ -51,10 +52,6 @@ const kubectlMap: Map = new Map([ ["1.20", "1.20.8"], ["1.21", bundledVersion] ]); -const packageMirrors: Map = new Map([ - ["default", "https://storage.googleapis.com/kubernetes-release/release"], - ["china", "https://mirror.azure.cn/kubernetes/kubectl"] -]); let bundledPath: string; const initScriptVersionString = "# lens-initscript v3\n"; @@ -389,12 +386,9 @@ export class Kubectl { } protected getDownloadMirror() { - const mirror = packageMirrors.get(UserStore.getInstance().downloadMirror); + // MacOS packages are only available from default - if (mirror) { - return mirror; - } - - return packageMirrors.get("default"); // MacOS packages are only available from default + return packageMirrors.get(UserStore.getInstance().downloadMirror) + ?? packageMirrors.get(defaultPackageMirror); } } diff --git a/src/migrations/hotbar-store/5.0.0-alpha.2.ts b/src/migrations/hotbar-store/5.0.0-alpha.2.ts index 0a662b466a..6eeb759779 100644 --- a/src/migrations/hotbar-store/5.0.0-alpha.2.ts +++ b/src/migrations/hotbar-store/5.0.0-alpha.2.ts @@ -27,7 +27,8 @@ import type { MigrationDeclaration } from "../helpers"; export default { version: "5.0.0-alpha.2", run(store) { - const hotbars = (store.get("hotbars") || []) as Hotbar[]; + const rawHotbars = store.get("hotbars"); + const hotbars: Hotbar[] = Array.isArray(rawHotbars) ? rawHotbars : []; store.set("hotbars", hotbars.map((hotbar) => ({ id: uuid.v4(), diff --git a/src/migrations/hotbar-store/5.0.0-beta.10.ts b/src/migrations/hotbar-store/5.0.0-beta.10.ts index 7d7abc01b7..a9a0de11b2 100644 --- a/src/migrations/hotbar-store/5.0.0-beta.10.ts +++ b/src/migrations/hotbar-store/5.0.0-beta.10.ts @@ -46,9 +46,20 @@ interface PartialHotbar { export default { version: "5.0.0-beta.10", run(store) { - const hotbars = (store.get("hotbars") as Hotbar[] ?? []).filter(Boolean); + const rawHotbars = store.get("hotbars"); + const hotbars: Hotbar[] = Array.isArray(rawHotbars) ? rawHotbars.filter(h => h && typeof h === "object") : []; const userDataPath = app.getPath("userData"); + // Hotbars might be empty, if some of the previous migrations weren't run + if (hotbars.length === 0) { + const hotbar = getEmptyHotbar("default"); + const { metadata: { uid, name, source } } = catalogEntity; + + hotbar.items[0] = { entity: { uid, name, source } }; + + hotbars.push(hotbar); + } + try { const workspaceStoreData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(userDataPath, "lens-workspace-store.json")); const { clusters }: ClusterStoreModel = fse.readJSONSync(path.join(userDataPath, "lens-cluster-store.json")); @@ -144,12 +155,13 @@ export default { } } - store.set("hotbars", hotbars); } catch (error) { // ignore files being missing if (error.code !== "ENOENT") { throw error; } } + + store.set("hotbars", hotbars); } } as MigrationDeclaration; diff --git a/src/migrations/hotbar-store/5.0.0-beta.5.ts b/src/migrations/hotbar-store/5.0.0-beta.5.ts index f9cfd77262..40f36e13fe 100644 --- a/src/migrations/hotbar-store/5.0.0-beta.5.ts +++ b/src/migrations/hotbar-store/5.0.0-beta.5.ts @@ -26,25 +26,27 @@ import type { MigrationDeclaration } from "../helpers"; export default { version: "5.0.0-beta.5", run(store) { - const hotbars: Hotbar[] = store.get("hotbars"); + const rawHotbars = store.get("hotbars"); + const hotbars: Hotbar[] = Array.isArray(rawHotbars) ? rawHotbars : []; - hotbars.forEach((hotbar, hotbarIndex) => { - hotbar.items.forEach((item, itemIndex) => { + for (const hotbar of hotbars) { + for (let i = 0; i < hotbar.items.length; i += 1) { + const item = hotbar.items[i]; const entity = catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item?.entity.uid); if (!entity) { // Clear disabled item - hotbars[hotbarIndex].items[itemIndex] = null; + hotbar.items[i] = null; } else { // Save additional data - hotbars[hotbarIndex].items[itemIndex].entity = { + hotbar.items[i].entity = { ...item.entity, name: entity.metadata.name, source: entity.metadata.source }; } - }); - }); + } + } store.set("hotbars", hotbars); } diff --git a/src/renderer/components/+catalog/catalog-entity-details.module.css b/src/renderer/components/+catalog/catalog-entity-details.module.css new file mode 100644 index 0000000000..27b2c69701 --- /dev/null +++ b/src/renderer/components/+catalog/catalog-entity-details.module.css @@ -0,0 +1,50 @@ +/** + * 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. + */ + +.entityDetails { + /* Move Details panel under TopBar line */ + height: calc(100% - var(--main-layout-header)); + margin-top: var(--main-layout-header); +} + +.metadata { + margin-right: var(--margin); +} + +.entityIcon { + margin-right: calc(var(--margin) * 3); + + .avatar { + :global(.MuiAvatar-root) { + font-size: 3ch; + } + } + + .hint { + text-align: center; + font-size: var(--font-size-small); + text-transform: uppercase; + margin-top: var(--margin); + cursor: default; + user-select: none; + opacity: 0.5; + } +} diff --git a/src/renderer/components/+catalog/catalog-entity-details.tsx b/src/renderer/components/+catalog/catalog-entity-details.tsx index bff73a3fa2..40bc63fdee 100644 --- a/src/renderer/components/+catalog/catalog-entity-details.tsx +++ b/src/renderer/components/+catalog/catalog-entity-details.tsx @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./catalog-entity-details.scss"; +import styles from "./catalog-entity-details.module.css"; import React, { Component } from "react"; import { observer } from "mobx-react"; import { Drawer, DrawerItem } from "../drawer"; @@ -30,6 +30,7 @@ import { CatalogEntityDetailRegistry } from "../../../extensions/registries"; import { HotbarIcon } from "../hotbar/hotbar-icon"; import type { CatalogEntityItem } from "./catalog-entity-item"; import { isDevelopment } from "../../../common/vars"; +import { cssNames } from "../../utils"; interface Props { item: CatalogEntityItem | null | undefined; @@ -57,8 +58,8 @@ export class CatalogEntityDetails extends Component {showDetails && ( -
-
+
+
extends Component item.onRun()} size={128} data-testid="detail-panel-hot-bar-icon" + className={styles.avatar} /> {item?.enabled && ( -
+
Click to open
)}
-
+
{item.name} @@ -114,7 +116,7 @@ export class CatalogEntityDetails extends Component extends React.Comp } - removeContent={} + addContent={} + removeContent={} /> ); diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 594bb610e3..06fdb5b21b 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -220,7 +220,7 @@ export class Catalog extends React.Component { small className={styles.pinIcon} material={!isItemInHotbar && "push_pin"} - svg={isItemInHotbar && "unpin"} + svg={isItemInHotbar && "push_off"} tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"} onClick={prevDefault(() => isItemInHotbar ? this.removeFromHotbar(item) : this.addToHotbar(item))} /> diff --git a/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx b/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx index 7d679e4d81..00f6f3600f 100644 --- a/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx +++ b/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx @@ -26,7 +26,7 @@ import { MenuItem } from "../menu"; import type { CatalogEntity } from "../../api/catalog-entity"; export function HotbarToggleMenuItem(props: { entity: CatalogEntity, addContent: ReactNode, removeContent: ReactNode }) { - const store = HotbarStore.getInstance(false); + const store = HotbarStore.getInstance(); const add = () => store.addToHotbar(props.entity); const remove = () => store.removeFromHotbar(props.entity.getId()); const [itemInHotbar, setItemInHotbar] = useState(store.isAddedToActive(props.entity)); diff --git a/src/renderer/components/+cluster/cluster-issues.scss b/src/renderer/components/+cluster/cluster-issues.module.css similarity index 66% rename from src/renderer/components/+cluster/cluster-issues.scss rename to src/renderer/components/+cluster/cluster-issues.module.css index aadb46ee39..fc421f1401 100644 --- a/src/renderer/components/+cluster/cluster-issues.scss +++ b/src/renderer/components/+cluster/cluster-issues.module.css @@ -19,61 +19,56 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -.ClusterIssues { + .ClusterIssues { min-height: 350px; position: relative; - grid-column-start: 1; - grid-column-end: 3; + grid-area: issues; + padding: calc(var(--margin) * 2) 0; + background: var(--contentColor); - @include media("<1024px") { - grid-column-start: 1!important; - grid-column-end: 1!important; + &:global(.OnlyClusterIssues) { + grid-row: row1-start / row2-end; + } + + @media (max-width: 1150px) { + grid-area: unset; + + &:global(.OnlyClusterIssues) { + grid-row: auto; + } } .SubHeader { - .Icon { + padding-top: 0; + padding-bottom: 0; + + :global(.Icon) { font-size: 130%; - color: $colorError; + color: var(--colorError); } } - .Table { - .TableHead { - background-color: transparent; - border-bottom: 1px solid $borderFaintColor; - - .TableCell { - padding-top: 0; - } - } - - .TableCell { - white-space: nowrap; - text-overflow: ellipsis; - - &.message { - flex-grow: 3; - } - - &.object { - flex-grow: 2; - } - } + .message { + white-space: nowrap; + text-overflow: ellipsis; + flex-grow: 3; } - .no-issues { - .Icon { - color: white; - } + .object { + white-space: nowrap; + text-overflow: ellipsis; + flex-grow: 2; + } + .noIssues { .ok-title { font-size: large; - color: $textColorAccent; + color: var(--textColorAccent); font-weight: bold; } + + .allGood { + color: white; + } } } - -.OnlyClusterIssues { - grid-row: row1-start / row2-end; -} diff --git a/src/renderer/components/+cluster/cluster-issues.tsx b/src/renderer/components/+cluster/cluster-issues.tsx index 3f9b5c6b90..6165fde8bd 100644 --- a/src/renderer/components/+cluster/cluster-issues.tsx +++ b/src/renderer/components/+cluster/cluster-issues.tsx @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./cluster-issues.scss"; +import styles from "./cluster-issues.module.css"; import React from "react"; import { observer } from "mobx-react"; @@ -121,10 +121,10 @@ export class ClusterIssues extends React.Component { selected={selfLink === kubeSelectedUrlParam.get()} onClick={prevDefault(() => toggleDetails(selfLink))} > - + {message} - + {getName()} @@ -148,8 +148,8 @@ export class ClusterIssues extends React.Component { if (!warnings.length) { return ( -
-
+
+
No issues found
Everything is fine in the Cluster
@@ -158,7 +158,7 @@ export class ClusterIssues extends React.Component { return ( <> - + {" "} <>Warnings: {warnings.length} @@ -186,7 +186,7 @@ export class ClusterIssues extends React.Component { render() { return ( -
+
{this.renderContent()}
); diff --git a/src/renderer/components/+cluster/cluster-metric-switchers.tsx b/src/renderer/components/+cluster/cluster-metric-switchers.tsx index b80dd10527..aa2ff2985f 100644 --- a/src/renderer/components/+cluster/cluster-metric-switchers.tsx +++ b/src/renderer/components/+cluster/cluster-metric-switchers.tsx @@ -19,8 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./cluster-metric-switchers.scss"; - import React from "react"; import { observer } from "mobx-react"; import { nodesStore } from "../+nodes/nodes.store"; @@ -36,7 +34,7 @@ export const ClusterMetricSwitchers = observer(() => { const disableMetrics = !metricsValues.length; return ( -
+
{
-
+
{ } if (!memoryCapacity || !cpuCapacity) { - return ; + return ; } return ( @@ -106,12 +106,13 @@ export const ClusterMetrics = observer(() => { timeLabelStep={5} showLegend={false} plugins={[ZebraStripes]} + className={styles.chart} /> ); }; return ( -
+
{renderMetrics()}
diff --git a/src/renderer/components/+cluster/cluster-overview.scss b/src/renderer/components/+cluster/cluster-overview.module.css similarity index 80% rename from src/renderer/components/+cluster/cluster-overview.scss rename to src/renderer/components/+cluster/cluster-overview.module.css index 8dba6d65e8..7700ec038b 100644 --- a/src/renderer/components/+cluster/cluster-overview.scss +++ b/src/renderer/components/+cluster/cluster-overview.module.css @@ -20,28 +20,22 @@ */ .ClusterOverview { - $gridGap: $margin * 2; - position: relative; height: 100%; min-height: 650px; display: grid; - grid-gap: $gridGap; - grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); + grid-gap: calc(var(--margin) * 2); + grid-template-areas: + "barcharts piechars" + "issues issues"; + grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); grid-template-rows: [row1-start] 1fr [row2-start] 1fr; - @include media(">1600px") { - grid-template-columns: 1fr 1fr; - } - - > *:not(.Spinner) { - padding: $gridGap; - background: $contentColor; - - > .SubHeader { - padding-top: 0; - } + @media (max-width: 1150px) { + grid-template-columns: minmax(500px, 1fr); + grid-template-rows: 1fr; + grid-template-areas: none; } } diff --git a/src/renderer/components/+cluster/cluster-overview.tsx b/src/renderer/components/+cluster/cluster-overview.tsx index 248c7de427..b4fa25da45 100644 --- a/src/renderer/components/+cluster/cluster-overview.tsx +++ b/src/renderer/components/+cluster/cluster-overview.tsx @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./cluster-overview.scss"; +import styles from "./cluster-overview.module.css"; import React from "react"; import { reaction } from "mobx"; @@ -101,7 +101,7 @@ export class ClusterOverview extends React.Component { return ( -
+
{this.renderClusterOverview(isLoaded, isMetricHidden)}
diff --git a/src/renderer/components/+catalog/catalog-entity-details.scss b/src/renderer/components/+cluster/cluster-pie-charts.module.css similarity index 73% rename from src/renderer/components/+catalog/catalog-entity-details.scss rename to src/renderer/components/+cluster/cluster-pie-charts.module.css index f63e7c3ba3..1998c11989 100644 --- a/src/renderer/components/+catalog/catalog-entity-details.scss +++ b/src/renderer/components/+cluster/cluster-pie-charts.module.css @@ -19,25 +19,15 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - .CatalogEntityDetails { - .EntityMetadata { - margin-right: $margin; - } - .EntityIcon.box.top.left { - margin-right: $margin * 2; +.empty { + background: var(--contentColor); + min-height: 280px; + text-align: center; + padding: calc(var(--padding) * 2) 0; +} - .IconHint { - text-align: center; - font-size: var(--font-size-small); - text-transform: uppercase; - margin-top: $margin; - cursor: default; - user-select: none; - opacity: 0.5; - } - - div * { - font-size: 1.5em; - } - } - } +.chart { + --flex-gap: calc(var(--padding) * 2); + background: var(--contentColor); + padding: calc(var(--padding) * 2) var(--padding); +} \ No newline at end of file diff --git a/src/renderer/components/+cluster/cluster-pie-charts.tsx b/src/renderer/components/+cluster/cluster-pie-charts.tsx index 76bb6967f6..5079866870 100644 --- a/src/renderer/components/+cluster/cluster-pie-charts.tsx +++ b/src/renderer/components/+cluster/cluster-pie-charts.tsx @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./cluster-pie-charts.scss"; +import styles from "./cluster-pie-charts.module.css"; import React from "react"; import { observer } from "mobx-react"; @@ -29,7 +29,7 @@ import { Icon } from "../icon"; import { nodesStore } from "../+nodes/nodes.store"; import { ChartData, PieChart } from "../chart"; import { ClusterNoMetrics } from "./cluster-no-metrics"; -import { bytesToUnits } from "../../utils"; +import { bytesToUnits, cssNames } from "../../utils"; import { ThemeStore } from "../../theme.store"; import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api"; @@ -173,8 +173,8 @@ export const ClusterPieCharts = observer(() => { }; return ( -
-
+
+
{ /> {cpuLimitsOverload && renderLimitWarning()}
-
+
{ /> {memoryLimitsOverload && renderLimitWarning()}
-
+
{ if (!nodes.length) { return ( -
+
No Nodes Available.
@@ -237,14 +237,14 @@ export const ClusterPieCharts = observer(() => { const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterOverviewStore.metrics); if (!memoryCapacity || !cpuCapacity || !podCapacity) { - return ; + return ; } return renderCharts(); }; return ( -
+
{renderContent()}
); diff --git a/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx b/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx new file mode 100644 index 0000000000..292f228cf9 --- /dev/null +++ b/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx @@ -0,0 +1,49 @@ +/** + * 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 React from "react"; +import { render } from "@testing-library/react"; +import { SecretDetails } from "../secret-details"; +import { Secret, SecretType } from "../../../../common/k8s-api/endpoints"; + +jest.mock("../../kube-object-meta/kube-object-meta"); + + +describe("SecretDetails tests", () => { + it("should show the visibility toggle when the secret value is ''", () => { + const secret = new Secret({ + apiVersion: "v1", + kind: "secret", + metadata: { + name: "test", + resourceVersion: "1", + uid: "uid" + }, + data: { + foobar: "", + }, + type: SecretType.Opaque, + }); + const result = render(); + + expect(result.getByTestId("foobar-secret-entry").querySelector(".Icon")).toBeDefined(); + }); +}); diff --git a/src/renderer/components/+config-secrets/secret-details.tsx b/src/renderer/components/+config-secrets/secret-details.tsx index a8f4555ee1..30aff8640f 100644 --- a/src/renderer/components/+config-secrets/secret-details.tsx +++ b/src/renderer/components/+config-secrets/secret-details.tsx @@ -22,14 +22,13 @@ import "./secret-details.scss"; import React from "react"; -import isEmpty from "lodash/isEmpty"; import { autorun, observable, makeObservable } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { DrawerItem, DrawerTitle } from "../drawer"; import { Input } from "../input"; import { Button } from "../button"; import { Notifications } from "../notifications"; -import { base64 } from "../../utils"; +import { base64, ObservableToggleSet } from "../../utils"; import { Icon } from "../icon"; import { secretsStore } from "./secrets.store"; import type { KubeObjectDetailsProps } from "../kube-object-details"; @@ -44,7 +43,7 @@ interface Props extends KubeObjectDetailsProps { export class SecretDetails extends React.Component { @observable isSaving = false; @observable data: { [name: string]: string } = {}; - @observable revealSecret: { [name: string]: boolean } = {}; + revealSecret = new ObservableToggleSet(); constructor(props: Props) { super(props); @@ -58,7 +57,7 @@ export class SecretDetails extends React.Component { if (secret) { this.data = secret.data; - this.revealSecret = {}; + this.revealSecret.clear(); } }) ]); @@ -82,6 +81,69 @@ export class SecretDetails extends React.Component { this.data[name] = encoded ? value : base64.encode(value); }; + renderSecret = ([name, value]: [string, string]) => { + let decodedVal: string | undefined; + + try { + decodedVal = base64.decode(value); + } catch { + /** + * The value failed to be decoded, so don't show the visibility + * toggle until the value is saved + */ + this.revealSecret.delete(name); + } + + const revealSecret = this.revealSecret.has(name); + + if (revealSecret && typeof decodedVal === "string") { + value = decodedVal; + } + + return ( +
+
{name}
+
+ this.editData(name, value, !revealSecret)} + /> + {typeof decodedVal === "string" && ( + this.revealSecret.toggle(name)} + /> + )} +
+
+ ); + }; + + renderData() { + const secrets = Object.entries(this.data); + + if (secrets.length === 0) { + return null; + } + + return ( + <> + + {secrets.map(this.renderSecret)} +
); } diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index 7c0e36639a..b8645dfbd1 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -89,11 +89,11 @@ export class CrdResources extends React.Component { searchFilters={[ item => item.getSearchFields(), ]} - renderHeaderTitle={crd.getResourceTitle()} + renderHeaderTitle={crd.getResourceKind()} customizeHeader={({ searchProps, ...headerPlaceholders }) => ({ searchProps: { ...searchProps, - placeholder: `Search ${crd.getResourceTitle()}...`, + placeholder: `${crd.getResourceKind()} search ...`, }, ...headerPlaceholders })} diff --git a/src/renderer/components/+cluster/cluster-metric-switchers.scss b/src/renderer/components/+network-port-forwards/port-forward-details.scss similarity index 90% rename from src/renderer/components/+cluster/cluster-metric-switchers.scss rename to src/renderer/components/+network-port-forwards/port-forward-details.scss index ef8dab156f..6fcc501884 100644 --- a/src/renderer/components/+cluster/cluster-metric-switchers.scss +++ b/src/renderer/components/+network-port-forwards/port-forward-details.scss @@ -19,10 +19,12 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -.ClusterMetricSwitchers { - margin-bottom: $margin * 2; +.PortForwardDetails { + .SubTitle { + text-transform: none + } - .metric-switch { - text-align: right; + .status { + @include port-forward-status-colors; } } \ No newline at end of file diff --git a/src/renderer/components/+network-port-forwards/port-forward-details.tsx b/src/renderer/components/+network-port-forwards/port-forward-details.tsx new file mode 100644 index 0000000000..4fa4b395f7 --- /dev/null +++ b/src/renderer/components/+network-port-forwards/port-forward-details.tsx @@ -0,0 +1,107 @@ +/** + * 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 "./port-forward-details.scss"; + +import React from "react"; +import { Link } from "react-router-dom"; +import type { PortForwardItem } from "../../port-forward"; +import { Drawer, DrawerItem } from "../drawer"; +import { cssNames } from "../../utils"; +import { podsApi, serviceApi } from "../../../common/k8s-api/endpoints"; +import { getDetailsUrl } from "../kube-detail-params"; +import { PortForwardMenu } from "./port-forward-menu"; + +interface Props { + portForward: PortForwardItem; + hideDetails(): void; +} + +export class PortForwardDetails extends React.Component { + + renderResourceName() { + const { portForward } = this.props; + const name = portForward.getName(); + const api = { + "service": serviceApi, + "pod": podsApi + }[portForward.kind]; + + if (!api) { + return ( + {name} + ); + } + + return ( + + {name} + + ); + } + + renderContent() { + const { portForward } = this.props; + + if (!portForward) return null; + + return ( +
+ + {this.renderResourceName()} + + + {portForward.getNs()} + + + {portForward.getKind()} + + + {portForward.getPort()} + + + {portForward.getForwardPort()} + + + {portForward.getStatus()} + +
+ ); + } + + render() { + const { hideDetails, portForward } = this.props; + const toolbar = ; + + return ( + + {this.renderContent()} + + ); + } +} diff --git a/src/renderer/components/+network-port-forwards/port-forwards.scss b/src/renderer/components/+network-port-forwards/port-forwards.scss index 0c048fc94d..50a3fd3f83 100644 --- a/src/renderer/components/+network-port-forwards/port-forwards.scss +++ b/src/renderer/components/+network-port-forwards/port-forwards.scss @@ -24,5 +24,10 @@ &.warning { @include table-cell-warning; } + + &.status { + @include port-forward-status-colors; + flex: 0.6; + } } } \ No newline at end of file diff --git a/src/renderer/components/+network-port-forwards/port-forwards.tsx b/src/renderer/components/+network-port-forwards/port-forwards.tsx index c056dab454..9fdeec4f6a 100644 --- a/src/renderer/components/+network-port-forwards/port-forwards.tsx +++ b/src/renderer/components/+network-port-forwards/port-forwards.tsx @@ -23,9 +23,13 @@ import "./port-forwards.scss"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; +import type { RouteComponentProps } from "react-router-dom"; import { ItemListLayout } from "../item-object-list/item-list-layout"; import { PortForwardItem, portForwardStore } from "../../port-forward"; import { PortForwardMenu } from "./port-forward-menu"; +import { PortForwardsRouteParams, portForwardsURL } from "../../../common/routes"; +import { PortForwardDetails } from "./port-forward-details"; +import { navigation } from "../../navigation"; enum columnId { name = "name", @@ -33,10 +37,14 @@ enum columnId { kind = "kind", port = "port", forwardPort = "forwardPort", + status = "status", +} + +interface Props extends RouteComponentProps { } @observer -export class PortForwards extends React.Component { +export class PortForwards extends React.Component { componentDidMount() { disposeOnUnmount(this, [ @@ -44,6 +52,32 @@ export class PortForwards extends React.Component { ]); } + get selectedPortForward() { + const { match: { params: { forwardport } } } = this.props; + + return portForwardStore.getById(forwardport); + } + + onDetails = (item: PortForwardItem) => { + if (item === this.selectedPortForward) { + this.hideDetails(); + } else { + this.showDetails(item); + } + }; + + showDetails = (item: PortForwardItem) => { + navigation.push(portForwardsURL({ + params: { + forwardport: String(item.getForwardPort()), + } + })); + }; + + hideDetails = () => { + navigation.push(portForwardsURL()); + }; + renderRemoveDialogMessage(selectedItems: PortForwardItem[]) { const forwardPorts = selectedItems.map(item => item.getForwardPort()).join(", "); @@ -68,6 +102,7 @@ export class PortForwards extends React.Component { [columnId.kind]: item => item.getKind(), [columnId.port]: item => item.getPort(), [columnId.forwardPort]: item => item.getForwardPort(), + [columnId.status]: item => item.getStatus(), }} searchFilters={[ item => item.getSearchFields(), @@ -79,6 +114,7 @@ export class PortForwards extends React.Component { { title: "Kind", className: "kind", sortBy: columnId.kind, id: columnId.kind }, { title: "Pod Port", className: "port", sortBy: columnId.port, id: columnId.port }, { title: "Local Port", className: "forwardPort", sortBy: columnId.forwardPort, id: columnId.forwardPort }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, ]} renderTableContents={item => [ item.getName(), @@ -86,6 +122,7 @@ export class PortForwards extends React.Component { item.getKind(), item.getPort(), item.getForwardPort(), + { title: item.getStatus(), className: item.getStatus().toLowerCase() }, ]} renderItemMenu={pf => ( ({ message: this.renderRemoveDialogMessage(selectedItems) })} + detailsItem={this.selectedPortForward} + onDetails={this.onDetails} /> + {this.selectedPortForward && ( + + )} ); } diff --git a/src/renderer/components/+network/network-mixins.scss b/src/renderer/components/+network/network-mixins.scss index 5dba700e69..edbf0fa73e 100644 --- a/src/renderer/components/+network/network-mixins.scss +++ b/src/renderer/components/+network/network-mixins.scss @@ -31,3 +31,15 @@ $service-status-color-list: ( } } } + +$port-forward-status-color-list: ( + active: $colorOk, +); + +@mixin port-forward-status-colors { + @each $status, $color in $port-forward-status-color-list { + &.#{$status} { + color: $color; + } + } +} diff --git a/src/renderer/components/+preferences/kubectl-binaries.tsx b/src/renderer/components/+preferences/kubectl-binaries.tsx index 226ef23c1e..85b699a840 100644 --- a/src/renderer/components/+preferences/kubectl-binaries.tsx +++ b/src/renderer/components/+preferences/kubectl-binaries.tsx @@ -27,17 +27,17 @@ import { observer } from "mobx-react"; import { bundledKubectlPath } from "../../../main/kubectl"; import { SelectOption, Select } from "../select"; import { FormSwitch, Switcher } from "../switch"; +import { packageMirrors } from "../../../common/user-store/preferences-helpers"; export const KubectlBinaries = observer(() => { const userStore = UserStore.getInstance(); const [downloadPath, setDownloadPath] = useState(userStore.downloadBinariesPath || ""); const [binariesPath, setBinariesPath] = useState(userStore.kubectlBinariesPath || ""); const pathValidator = downloadPath ? InputValidators.isPath : undefined; - - const downloadMirrorOptions: SelectOption[] = [ - { value: "default", label: "Default (Google)" }, - { value: "china", label: "China (Azure)" }, - ]; + const downloadMirrorOptions: SelectOption[] = Array.from( + packageMirrors.entries(), + ([value, { label, platforms }]) => ({ value, label, platforms }) + ); const save = () => { userStore.downloadBinariesPath = downloadPath; @@ -68,6 +68,7 @@ export const KubectlBinaries = observer(() => { value={userStore.downloadMirror} onChange={({ value }: SelectOption) => userStore.downloadMirror = value} disabled={!userStore.downloadKubectlBinaries} + isOptionDisabled={({ platforms }) => !platforms.has(process.platform)} themeName="lens" /> diff --git a/src/renderer/components/+user-management/+service-accounts/details.tsx b/src/renderer/components/+user-management/+service-accounts/details.tsx index 3cc12dd24d..ee19079c40 100644 --- a/src/renderer/components/+user-management/+service-accounts/details.tsx +++ b/src/renderer/components/+user-management/+service-accounts/details.tsx @@ -27,7 +27,7 @@ import React from "react"; import { Link } from "react-router-dom"; import { secretsStore } from "../../+config-secrets/secrets.store"; -import { Secret, ServiceAccount } from "../../../../common/k8s-api/endpoints"; +import { Secret, SecretType, ServiceAccount } from "../../../../common/k8s-api/endpoints"; import { DrawerItem, DrawerTitle } from "../../drawer"; import { Icon } from "../../icon"; import type { KubeObjectDetailsProps } from "../../kube-object-details"; @@ -124,7 +124,8 @@ export class ServiceAccountsDetails extends React.Component { uid: null, selfLink: null, resourceVersion: null - } + }, + type: SecretType.Opaque }); } diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx index e9c4442b15..31f3203113 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx @@ -28,7 +28,6 @@ import type { DaemonSet } from "../../../common/k8s-api/endpoints"; import { eventStore } from "../+events/event.store"; import { daemonSetStore } from "./daemonsets.store"; import { podsStore } from "../+workloads-pods/pods.store"; -import { nodesStore } from "../+nodes/nodes.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { Badge } from "../badge"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; @@ -63,7 +62,7 @@ export class DaemonSets extends React.Component { isConfigurable tableId="workload_daemonsets" className="DaemonSets" store={daemonSetStore} - dependentStores={[podsStore, nodesStore, eventStore]} + dependentStores={[podsStore, eventStore]} // status icon component uses event store sortingCallbacks={{ [columnId.name]: daemonSet => daemonSet.getName(), [columnId.namespace]: daemonSet => daemonSet.getNs(), diff --git a/src/renderer/components/+workloads-deployments/deployment-replicasets.tsx b/src/renderer/components/+workloads-deployments/deployment-replicasets.tsx index 13770b0abf..03c8c47890 100644 --- a/src/renderer/components/+workloads-deployments/deployment-replicasets.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-replicasets.tsx @@ -71,11 +71,11 @@ export class DeploymentReplicaSets extends React.Component { diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx index 28705fd53d..74cbb81d84 100644 --- a/src/renderer/components/+workloads-deployments/deployments.tsx +++ b/src/renderer/components/+workloads-deployments/deployments.tsx @@ -31,9 +31,6 @@ import { Icon } from "../icon"; import { DeploymentScaleDialog } from "./deployment-scale-dialog"; import { ConfirmDialog } from "../confirm-dialog"; import { deploymentStore } from "./deployments.store"; -import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; -import { podsStore } from "../+workloads-pods/pods.store"; -import { nodesStore } from "../+nodes/nodes.store"; import { eventStore } from "../+events/event.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { cssNames } from "../../utils"; @@ -79,7 +76,7 @@ export class Deployments extends React.Component { isConfigurable tableId="workload_deployments" className="Deployments" store={deploymentStore} - dependentStores={[replicaSetStore, podsStore, nodesStore, eventStore]} + dependentStores={[eventStore]} // status icon component uses event store sortingCallbacks={{ [columnId.name]: deployment => deployment.getName(), [columnId.namespace]: deployment => deployment.getNs(), diff --git a/src/renderer/components/+workloads-jobs/jobs.tsx b/src/renderer/components/+workloads-jobs/jobs.tsx index 6dc2362604..caa0b9b9f3 100644 --- a/src/renderer/components/+workloads-jobs/jobs.tsx +++ b/src/renderer/components/+workloads-jobs/jobs.tsx @@ -24,7 +24,6 @@ import "./jobs.scss"; import React from "react"; import { observer } from "mobx-react"; import type { RouteComponentProps } from "react-router"; -import { podsStore } from "../+workloads-pods/pods.store"; import { jobStore } from "./job.store"; import { eventStore } from "../+events/event.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; @@ -51,7 +50,7 @@ export class Jobs extends React.Component { isConfigurable tableId="workload_jobs" className="Jobs" store={jobStore} - dependentStores={[podsStore, eventStore]} + dependentStores={[eventStore]} // status icon component uses event store sortingCallbacks={{ [columnId.name]: job => job.getName(), [columnId.namespace]: job => job.getNs(), diff --git a/src/renderer/components/+workloads-pods/pod-details-list.tsx b/src/renderer/components/+workloads-pods/pod-details-list.tsx index b3dbe80fe7..e7e603375c 100644 --- a/src/renderer/components/+workloads-pods/pod-details-list.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-list.tsx @@ -51,26 +51,14 @@ interface Props extends OptionalProps { interface OptionalProps { maxCpu?: number; maxMemory?: number; - showTitle?: boolean; } @observer export class PodDetailsList extends React.Component { - static defaultProps: OptionalProps = { - showTitle: true - }; - private metricsWatcher = interval(120, () => { podsStore.loadKubeMetrics(this.props.owner.getNs()); }); - private sortingCallbacks = { - [sortBy.name]: (pod: Pod) => pod.getName(), - [sortBy.namespace]: (pod: Pod) => pod.getNs(), - [sortBy.cpu]: (pod: Pod) => podsStore.getPodKubeMetrics(pod).cpu, - [sortBy.memory]: (pod: Pod) => podsStore.getPodKubeMetrics(pod).memory, - }; - componentDidMount() { this.metricsWatcher.start(true); disposeOnUnmount(this, [ @@ -144,27 +132,43 @@ export class PodDetailsList extends React.Component { } render() { - const { pods, showTitle } = this.props; - const virtual = pods.length > 100; + const { pods } = this.props; - if (!pods.length && !podsStore.isLoaded) return ( -
- ); - if (!pods.length) return null; + if (!podsStore.isLoaded) { + return ( +
+ +
+ ); + } + + if (!pods.length) { + return null; + } + + const virtual = pods.length > 20; return (
- {showTitle && } +
pod.getName(), + [sortBy.namespace]: pod => pod.getNs(), + [sortBy.cpu]: pod => podsStore.getPodKubeMetrics(pod).cpu, + [sortBy.memory]: pod => podsStore.getPodKubeMetrics(pod).memory, + }} sortByDefault={{ sortBy: sortBy.cpu, orderBy: "desc" }} sortSyncWithUrl={false} getTableRow={this.getTableRow} + renderRow={!virtual && (pod => this.getTableRow(pod.getId()))} className="box grow" > @@ -176,9 +180,6 @@ export class PodDetailsList extends React.Component { Memory Status - { - !virtual && pods.map(pod => this.getTableRow(pod.getId())) - }
); diff --git a/src/renderer/components/+workloads-pods/pods.tsx b/src/renderer/components/+workloads-pods/pods.tsx index 448bfa75c8..8cb46e19c9 100644 --- a/src/renderer/components/+workloads-pods/pods.tsx +++ b/src/renderer/components/+workloads-pods/pods.tsx @@ -26,12 +26,11 @@ import { observer } from "mobx-react"; import { Link } from "react-router-dom"; import { podsStore } from "./pods.store"; import type { RouteComponentProps } from "react-router"; -import { volumeClaimStore } from "../+storage-volume-claims/volume-claim.store"; import { eventStore } from "../+events/event.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { nodesApi, Pod } from "../../../common/k8s-api/endpoints"; import { StatusBrick } from "../status-brick"; -import { cssNames, stopPropagation } from "../../utils"; +import { cssNames, getConvertedParts, stopPropagation } from "../../utils"; import toPairs from "lodash/toPairs"; import startCase from "lodash/startCase"; import kebabCase from "lodash/kebabCase"; @@ -94,11 +93,11 @@ export class Pods extends React.Component { return ( pod.getName(), + [columnId.name]: pod => getConvertedParts(pod.getName()), [columnId.namespace]: pod => pod.getNs(), [columnId.containers]: pod => pod.getContainers().length, [columnId.restarts]: pod => pod.getRestartsCount(), diff --git a/src/renderer/components/+workloads-replicasets/replicasets.tsx b/src/renderer/components/+workloads-replicasets/replicasets.tsx index 8eaaa8f7cb..07526e7014 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.tsx +++ b/src/renderer/components/+workloads-replicasets/replicasets.tsx @@ -33,6 +33,7 @@ import { MenuItem } from "../menu/menu"; import { Icon } from "../icon/icon"; import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog"; import type { ReplicaSetsRouteParams } from "../../../common/routes"; +import { eventStore } from "../+events/event.store"; enum columnId { name = "name", @@ -54,6 +55,7 @@ export class ReplicaSets extends React.Component { isConfigurable tableId="workload_replicasets" className="ReplicaSets" store={replicaSetStore} + dependentStores={[eventStore]} // status icon component uses event store sortingCallbacks={{ [columnId.name]: replicaSet => replicaSet.getName(), [columnId.namespace]: replicaSet => replicaSet.getNs(), diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx index c3b5b0e023..82704bbb91 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx @@ -27,7 +27,6 @@ import type { RouteComponentProps } from "react-router"; import type { StatefulSet } from "../../../common/k8s-api/endpoints"; import { podsStore } from "../+workloads-pods/pods.store"; import { statefulSetStore } from "./statefulset.store"; -import { nodesStore } from "../+nodes/nodes.store"; import { eventStore } from "../+events/event.store"; import type { KubeObjectMenuProps } from "../kube-object-menu"; import { KubeObjectListLayout } from "../kube-object-list-layout"; @@ -62,7 +61,7 @@ export class StatefulSets extends React.Component { isConfigurable tableId="workload_statefulsets" className="StatefulSets" store={statefulSetStore} - dependentStores={[podsStore, nodesStore, eventStore]} + dependentStores={[podsStore, eventStore]} // status icon component uses event store, details component uses podStore sortingCallbacks={{ [columnId.name]: statefulSet => statefulSet.getName(), [columnId.namespace]: statefulSet => statefulSet.getNs(), diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 59bf2ea065..7932b1f880 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -72,6 +72,7 @@ import type { ClusterId } from "../../common/cluster-types"; import { watchHistoryState } from "../remote-helpers/history-updater"; import { unmountComponentAtNode } from "react-dom"; import { PortForwardDialog } from "../port-forward"; +import { DeleteClusterDialog } from "./delete-cluster-dialog"; @observer export class App extends React.Component { @@ -112,13 +113,6 @@ export class App extends React.Component { window.location.reload(); }); - window.addEventListener("message", (ev: MessageEvent) => { - if (ev.data === "teardown") { - unmountComponentAtNode(rootElem); - window.location.href = "about:blank"; - } - }); - window.onbeforeunload = () => { logger.info(`[APP]: Unload dashboard, clusterId=${App.clusterId}, frameId=${frameId}`); @@ -230,6 +224,7 @@ export class App extends React.Component { + diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/lens-views.ts index 830fe20cb3..b4f8415e71 100644 --- a/src/renderer/components/cluster-manager/lens-views.ts +++ b/src/renderer/components/cluster-manager/lens-views.ts @@ -79,13 +79,7 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`); lensViews.delete(clusterId); - // Keep frame in DOM to avoid possible bugs when same cluster re-created after being removed. - // In that case for some reasons `webFrame.routingId` returns some previous frameId (usage in app.tsx) - // Issue: https://github.com/lensapp/lens/issues/811 - iframe.style.display = "none"; - iframe.dataset.meta = `${iframe.name} was removed at ${new Date().toLocaleString()}`; - iframe.removeAttribute("name"); - iframe.contentWindow.postMessage("teardown", "*"); + iframe.parentNode.removeChild(iframe); } export function refreshViews(visibleClusterId?: string) { diff --git a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx index 01cf35eec8..94ad6c79bd 100644 --- a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx +++ b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx @@ -116,6 +116,7 @@ export class DeleteClusterDialog extends React.Component { await requestMain(clusterDeleteHandler, cluster.id); } catch(error) { Notifications.error(`Cannot remove cluster, failed to process config file. ${error}`); + } finally { await requestMain(clusterClearDeletingHandler, cluster.id); } diff --git a/src/renderer/components/drawer/drawer.scss b/src/renderer/components/drawer/drawer.scss index 6598e156e4..86704ea956 100644 --- a/src/renderer/components/drawer/drawer.scss +++ b/src/renderer/components/drawer/drawer.scss @@ -30,6 +30,7 @@ background: $contentColor; box-shadow: 0 0 $unit * 2 $boxShadow; z-index: $zIndex-drawer; + height: 100%; &.left { left: 0; @@ -51,7 +52,6 @@ &.right { top: 0; width: var(--size); - height: 100%; } &.top, diff --git a/src/renderer/components/hotbar/hotbar-entity-icon.tsx b/src/renderer/components/hotbar/hotbar-entity-icon.tsx index 24405da556..b3ed9df942 100644 --- a/src/renderer/components/hotbar/hotbar-entity-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-entity-icon.tsx @@ -44,20 +44,16 @@ interface Props extends DOMAttributes { @observer export class HotbarEntityIcon extends React.Component { - @observable private contextMenu: CatalogEntityContextMenuContext; + @observable private contextMenu: CatalogEntityContextMenuContext = { + menuItems: [], + navigate: (url: string) => navigate(url), + }; constructor(props: Props) { super(props); makeObservable(this); } - componentDidMount() { - this.contextMenu = { - menuItems: [], - navigate: (url: string) => navigate(url), - }; - } - get kindIcon() { const className = "badge"; const category = catalogCategoryRegistry.getCategoryForEntity(this.props.entity); diff --git a/src/renderer/components/icon/push_off.svg b/src/renderer/components/icon/push_off.svg new file mode 100644 index 0000000000..07221c1cc2 --- /dev/null +++ b/src/renderer/components/icon/push_off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/components/icon/unpin.svg b/src/renderer/components/icon/unpin.svg deleted file mode 100644 index f685eb5e92..0000000000 --- a/src/renderer/components/icon/unpin.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/src/renderer/components/layout/main-layout.tsx b/src/renderer/components/layout/main-layout.tsx index 34b0c1ed95..e8316bfd3c 100755 --- a/src/renderer/components/layout/main-layout.tsx +++ b/src/renderer/components/layout/main-layout.tsx @@ -26,7 +26,7 @@ import { observer } from "mobx-react"; import { cssNames } from "../../utils"; import { ErrorBoundary } from "../error-boundary"; import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor"; -import { sidebarStorage } from "./sidebar-storage"; +import { defaultSidebarWidth, sidebarStorage } from "./sidebar-storage"; interface Props { sidebar: React.ReactNode; @@ -46,7 +46,6 @@ export class MainLayout extends React.Component { }; render() { - const { onSidebarResize } = this; const { className, footer, children, sidebar } = this.props; const { width: sidebarWidth } = sidebarStorage.get(); const style = { "--sidebar-width": `${sidebarWidth}px` } as React.CSSProperties; @@ -60,7 +59,8 @@ export class MainLayout extends React.Component { placement={ResizeSide.TRAILING} growthDirection={ResizeGrowthDirection.LEFT_TO_RIGHT} getCurrentExtent={() => sidebarWidth} - onDrag={onSidebarResize} + onDrag={this.onSidebarResize} + onDoubleClick={() => this.onSidebarResize(defaultSidebarWidth)} minExtent={120} maxExtent={400} /> diff --git a/src/renderer/components/layout/sidebar-storage.ts b/src/renderer/components/layout/sidebar-storage.ts index 663cb7c177..162ca3e6b7 100644 --- a/src/renderer/components/layout/sidebar-storage.ts +++ b/src/renderer/components/layout/sidebar-storage.ts @@ -28,7 +28,9 @@ export interface SidebarStorageState { } } +export const defaultSidebarWidth = 200; + export const sidebarStorage = createStorage("sidebar", { - width: 200, + width: defaultSidebarWidth, expanded: {}, }); diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index ed9b76a8d1..0cde9b3d62 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -42,6 +42,9 @@ import * as routes from "../../../common/routes"; import { Config } from "../+config"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { HotbarIcon } from "../hotbar/hotbar-icon"; +import { makeObservable, observable } from "mobx"; +import type { CatalogEntityContextMenuContext } from "../../../common/catalog"; +import { HotbarStore } from "../../../common/hotbar-store"; interface Props { className?: string; @@ -50,6 +53,15 @@ interface Props { @observer export class Sidebar extends React.Component { static displayName = "Sidebar"; + @observable private contextMenu: CatalogEntityContextMenuContext = { + menuItems: [], + navigate, + }; + + constructor(props: Props) { + super(props); + makeObservable(this); + } async componentDidMount() { crdStore.reloadAll(); @@ -194,6 +206,20 @@ export class Sidebar extends React.Component { src={spec.icon?.src} className="mr-5" onClick={() => navigate("/")} + menuItems={this.contextMenu.menuItems} + onMenuOpen={() => { + const hotbarStore = HotbarStore.getInstance(); + const isAddedToActive = HotbarStore.getInstance().isAddedToActive(this.clusterEntity); + const title = isAddedToActive + ? "Remove from Hotbar" + : "Add to Hotbar"; + const onClick = isAddedToActive + ? () => hotbarStore.removeFromHotbar(metadata.uid) + : () => hotbarStore.addToHotbar(this.clusterEntity); + + this.contextMenu.menuItems = [{ title, onClick }]; + this.clusterEntity.onContextMenuOpen(this.contextMenu); + }} />
{metadata.name} diff --git a/src/renderer/components/table/__tests__/getSorted.test.ts b/src/renderer/components/table/__tests__/getSorted.test.ts new file mode 100644 index 0000000000..7480727ce1 --- /dev/null +++ b/src/renderer/components/table/__tests__/getSorted.test.ts @@ -0,0 +1,155 @@ +/** + * 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 { cloneDeep } from "lodash"; +import { getSorted } from "../sorting"; + +describe("Table tests", () => { + describe("getSorted", () => { + it.each([undefined, 5, "", true, {}, []])("should not sort since %j is not a function", () => { + expect(getSorted([1, 2, 4, 3], undefined, "asc")).toStrictEqual([1, 2, 4, 3]); + }); + + it("should sort numerically asc and not touch the original list", () => { + const i = [1, 2, 4, 3]; + + expect(getSorted(i, v => v, "asc")).toStrictEqual([1, 2, 3, 4]); + expect(i).toStrictEqual([1, 2, 4, 3]); + }); + + it("should sort numerically desc and not touch the original list", () => { + const i = [1, 2, 4, 3]; + + expect(getSorted(i, v => v, "desc")).toStrictEqual([4, 3, 2, 1]); + expect(i).toStrictEqual([1, 2, 4, 3]); + }); + + it("should sort numerically asc (by defaul) and not touch the original list", () => { + const i = [1, 2, 4, 3]; + + expect(getSorted(i, v => v, "foobar")).toStrictEqual([1, 2, 3, 4]); + expect(i).toStrictEqual([1, 2, 4, 3]); + }); + + describe("multi-part", () => { + it("should sort each part by its order", () => { + const i = ["a", "c", "b.1", "b.2", "d"]; + + expect(getSorted(i, v => v.split("."), "desc")).toStrictEqual(["d", "c", "b.2", "b.1", "a"]); + expect(i).toStrictEqual(["a", "c", "b.1", "b.2", "d"]); + }); + + it("should be a stable sort", () => { + const i = [{ + val: "a", + k: 1, + }, { + val: "c", + k: 2 + }, { + val: "b.1", + k: 3 + }, { + val: "b.2", + k: 4 + }, { + val: "d", + k: 5 + }, { + val: "b.2", + k: -10 + }]; + const dup = cloneDeep(i); + const expected = [ + { + val: "a", + k: 1, + }, { + val: "b.1", + k: 3 + }, { + val: "b.2", + k: 4 + }, { + val: "b.2", + k: -10 + }, { + val: "c", + k: 2 + }, { + val: "d", + k: 5 + }, + ]; + + expect(getSorted(i, ({ val }) => val.split("."), "asc")).toStrictEqual(expected); + expect(i).toStrictEqual(dup); + }); + + it("should be a stable sort #2", () => { + const i = [{ + val: "a", + k: 1, + }, { + val: "b.2", + k: -10 + }, { + val: "c", + k: 2 + }, { + val: "b.1", + k: 3 + }, { + val: "b.2", + k: 4 + }, { + val: "d", + k: 5 + }]; + const dup = cloneDeep(i); + const expected = [ + { + val: "a", + k: 1, + }, { + val: "b.1", + k: 3 + }, { + val: "b.2", + k: -10 + }, { + val: "b.2", + k: 4 + }, { + val: "c", + k: 2 + }, { + val: "d", + k: 5 + }, + ]; + + expect(getSorted(i, ({ val }) => val.split("."), "asc")).toStrictEqual(expected); + expect(i).toStrictEqual(dup); + }); + }); + }); +}); diff --git a/src/renderer/components/table/sorting.ts b/src/renderer/components/table/sorting.ts new file mode 100644 index 0000000000..ca9ea959fc --- /dev/null +++ b/src/renderer/components/table/sorting.ts @@ -0,0 +1,68 @@ +/** + * 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 type { TableSortCallback } from "./table"; +import { array, Ordering, rectifyOrdering, sortCompare } from "../../utils"; + +export function getSorted(rawItems: T[], sortingCallback: TableSortCallback | undefined, orderByRaw: string): T[] { + if (typeof sortingCallback !== "function") { + return rawItems; + } + + const orderBy = orderByRaw === "asc" || orderByRaw === "desc" ? orderByRaw : "asc"; + const sortData = rawItems.map((item, index) => ({ + index, + sortBy: sortingCallback(item), + })); + + sortData.sort((left, right) => { + if (!Array.isArray(left.sortBy) && !Array.isArray(right.sortBy)) { + return rectifyOrdering(sortCompare(left.sortBy, right.sortBy), orderBy); + } + + const leftSortBy = [left.sortBy].flat(); + const rightSortBy = [right.sortBy].flat(); + const zipIter = array.zipStrict(leftSortBy, rightSortBy); + let r = zipIter.next(); + + for (; r.done === false; r = zipIter.next()) { + const [nextL, nextR] = r.value; + + const sortOrder = rectifyOrdering(sortCompare(nextL, nextR), orderBy); + + if (sortOrder !== Ordering.EQUAL) { + return sortOrder; + } + } + + const [leftRest, rightRest] = r.value; + + return leftRest.length - rightRest.length; + }); + + const res = []; + + for (const { index } of sortData) { + res.push(rawItems[index]); + } + + return res; +} diff --git a/src/renderer/components/table/table.tsx b/src/renderer/components/table/table.tsx index bb5e63f570..6d71ba78a2 100644 --- a/src/renderer/components/table/table.tsx +++ b/src/renderer/components/table/table.tsx @@ -22,9 +22,8 @@ import "./table.scss"; import React from "react"; -import { orderBy } from "lodash"; import { observer } from "mobx-react"; -import { boundMethod, cssNames, noop } from "../../utils"; +import { boundMethod, cssNames } from "../../utils"; import { TableRow, TableRowElem, TableRowProps } from "./table-row"; import { TableHead, TableHeadElem, TableHeadProps } from "./table-head"; import type { TableCellElem } from "./table-cell"; @@ -32,6 +31,7 @@ import { VirtualList } from "../virtual-list"; import { createPageParam } from "../../navigation"; import { getSortParams, setSortParams } from "./table.storage"; import { computed, makeObservable } from "mobx"; +import { getSorted } from "./sorting"; export type TableSortBy = string; export type TableOrderBy = "asc" | "desc" | string; @@ -57,9 +57,26 @@ export interface TableProps extends React.DOMAttributes { onSort?: (params: TableSortParams) => void; // callback on sort change, default: global sync with url noItems?: React.ReactNode; // Show no items state table list is empty selectedItemId?: string; // Allows to scroll list to selected item - virtual?: boolean; // Use virtual list component to render only visible rows - rowPadding?: string; - rowLineHeight?: string; + + /** + * Use virtual list component to render only visible rows. By default uses a + * auto sizer to fill available height + */ + virtual?: boolean; + /** + * Only used when virtual is true. Sets the virtual list to be a fixed height. + * Needed when used in contexts that already have a parent component that + * is `overflow-y: scroll`, + */ + virtualHeight?: number; + /** + * Row padding in pixels + */ + rowPadding?: number; + /** + * Row line height in pixels + */ + rowLineHeight?: number; customRowHeights?: (item: Item, lineHeight: number, paddings: number) => number; getTableRow?: (uid: string) => React.ReactElement; renderRow?: (item: Item) => React.ReactElement; @@ -78,9 +95,10 @@ export class Table extends React.Component> { static defaultProps: TableProps = { scrollable: true, autoSize: true, - rowPadding: "8px", - rowLineHeight: "17px", + rowPadding: 8, + rowLineHeight: 17, sortSyncWithUrl: true, + customRowHeights: (item, lineHeight, paddings) => lineHeight + paddings, }; constructor(props: TableProps) { @@ -92,16 +110,22 @@ export class Table extends React.Component> { const { sortable, tableId } = this.props; if (sortable && !tableId) { - console.error("[Table]: sorted table requires props.tableId to be specified"); + console.error("Table must have props.tableId if props.sortable is specified"); } } + @computed get isSortable() { + const { sortable, tableId } = this.props; + + return Boolean(sortable && tableId); + } + @computed get sortParams() { return Object.assign({}, this.props.sortByDefault, getSortParams(this.props.tableId)); } renderHead() { - const { sortable, children } = this.props; + const { children } = this.props; const content = React.Children.toArray(children) as (TableRowElem | TableHeadElem)[]; const headElem: React.ReactElement = content.find(elem => elem.type === TableHead); @@ -109,7 +133,7 @@ export class Table extends React.Component> { return null; } - if (sortable) { + if (this.isSortable) { const columns = React.Children.toArray(headElem.props.children) as TableCellElem[]; return React.cloneElement(headElem, { @@ -136,14 +160,12 @@ export class Table extends React.Component> { return headElem; } - getSorted(items: any[]) { - const { sortBy, orderBy: order } = this.sortParams; - const sortingCallback = this.props.sortable[sortBy] || noop; + getSorted(rawItems: Item[]) { + const { sortBy, orderBy: orderByRaw } = this.sortParams; - return orderBy(items, sortingCallback, order as any); + return getSorted(rawItems, this.props.sortable[sortBy], orderByRaw); } - @boundMethod protected onSort({ sortBy, orderBy }: TableSortParams) { setSortParams(this.props.tableId, { sortBy, orderBy }); const { sortSyncWithUrl, onSort } = this.props; @@ -153,9 +175,7 @@ export class Table extends React.Component> { orderByUrlParam.set(orderBy); } - if (onSort) { - onSort({ sortBy, orderBy }); - } + onSort?.({ sortBy, orderBy }); } @boundMethod @@ -183,18 +203,19 @@ export class Table extends React.Component> { } renderRows() { - const { sortable, noItems, virtual, customRowHeights, rowLineHeight, rowPadding, items, getTableRow, selectedItemId, className } = this.props; + const { + noItems, virtual, customRowHeights, rowLineHeight, rowPadding, items, + getTableRow, selectedItemId, className, virtualHeight + } = this.props; const content = this.getContent(); let rows: React.ReactElement[] = content.filter(elem => elem.type === TableRow); let sortedItems = rows.length ? rows.map(row => row.props.sortItem) : [...items]; - if (sortable) { + if (this.isSortable) { sortedItems = this.getSorted(sortedItems); if (rows.length) { - rows = sortedItems.map(item => rows.find(row => { - return item == row.props.sortItem; - })); + rows = sortedItems.map(item => rows.find(row => item == row.props.sortItem)); } } @@ -203,15 +224,7 @@ export class Table extends React.Component> { } if (virtual) { - const lineHeight = parseFloat(rowLineHeight); - const padding = parseFloat(rowPadding); - let rowHeights: number[] = Array(items.length).fill(lineHeight + padding * 2); - - if (customRowHeights) { - rowHeights = sortedItems.map(item => { - return customRowHeights(item, lineHeight, padding * 2); - }); - } + const rowHeights = sortedItems.map(item => customRowHeights(item, rowLineHeight, rowPadding * 2)); return ( extends React.Component> { getRow={getTableRow} selectedItemId={selectedItemId} className={className} + fixedHeight={virtualHeight} /> ); } @@ -228,15 +242,13 @@ export class Table extends React.Component> { } render() { - const { selectable, scrollable, sortable, autoSize, virtual } = this.props; - let { className } = this.props; - - className = cssNames("Table flex column", className, { - selectable, scrollable, sortable, autoSize, virtual, + const { selectable, scrollable, autoSize, virtual, className } = this.props; + const classNames = cssNames("Table flex column", className, { + selectable, scrollable, sortable: this.isSortable, autoSize, virtual, }); return ( -
+
{this.renderHead()} {this.renderRows()}
diff --git a/src/renderer/components/virtual-list/virtual-list.tsx b/src/renderer/components/virtual-list/virtual-list.tsx index 36f62dff93..be3f2fae61 100644 --- a/src/renderer/components/virtual-list/virtual-list.tsx +++ b/src/renderer/components/virtual-list/virtual-list.tsx @@ -44,6 +44,12 @@ interface Props { getRow?: (uid: string | number) => React.ReactElement; onScroll?: (props: ListOnScrollProps) => void; outerRef?: React.Ref + + /** + * If specified then AutoSizer will not be used and instead a fixed height + * virtual list will be rendered + */ + fixedHeight?: number; } interface State { @@ -98,34 +104,45 @@ export class VirtualList extends Component { this.listRef.current?.scrollToItem(index, align); }; - render() { - const { width, className, items, getRow, onScroll, outerRef } = this.props; + renderList(height: number | undefined) { + const { width, items, getRow, onScroll, outerRef } = this.props; const { overscanCount } = this.state; - const rowData: RowData = { - items, - getRow - }; + + return ( + + {Row} + + ); + } + + render() { + const { className, fixedHeight } = this.props; return (
- - {({ height }) => ( - - {Row} - - )} - + { + typeof fixedHeight === "number" + ? this.renderList(fixedHeight) + : ( + + {({ height }) => this.renderList(height)} + + ) + }
); } diff --git a/src/renderer/navigation/events.ts b/src/renderer/navigation/events.ts index eaf3741b34..6435bfde1f 100644 --- a/src/renderer/navigation/events.ts +++ b/src/renderer/navigation/events.ts @@ -63,6 +63,7 @@ function bindClusterManagerRouteEvents() { ipcRendererOn(IpcRendererNavigationEvents.NAVIGATE_IN_APP, (event, url: string) => { logger.info(`[IPC]: navigate to ${url}`, { currentLocation: location.href }); navigate(url); + window.focus(); // make sure that the main frame is focused }); } diff --git a/src/renderer/port-forward/port-forward-dialog.tsx b/src/renderer/port-forward/port-forward-dialog.tsx index 6ba05ec1c4..7299228789 100644 --- a/src/renderer/port-forward/port-forward-dialog.tsx +++ b/src/renderer/port-forward/port-forward-dialog.tsx @@ -32,6 +32,7 @@ import { cssNames } from "../utils"; import { addPortForward, modifyPortForward } from "./port-forward.store"; import type { ForwardedPort } from "./port-forward-item"; import { openPortForward } from "."; +import { Checkbox } from "../components/checkbox"; interface Props extends Partial { } @@ -48,7 +49,6 @@ const dialogState = observable.object({ @observer export class PortForwardDialog extends Component { - @observable ready = false; @observable currentPort = 0; @observable desiredPort = 0; @@ -57,7 +57,7 @@ export class PortForwardDialog extends Component { makeObservable(this); } - static open(portForward: ForwardedPort, options : PortForwardDialogOpenOptions = { openInBrowser: false }) { + static open(portForward: ForwardedPort, options: PortForwardDialogOpenOptions = { openInBrowser: false }) { dialogState.isOpen = true; dialogState.data = portForward; dialogState.openInBrowser = options.openInBrowser; @@ -80,16 +80,13 @@ export class PortForwardDialog extends Component { this.currentPort = +portForward.forwardPort; this.desiredPort = this.currentPort; - this.ready = this.currentPort ? false : true; }; onClose = () => { - this.ready = false; }; changePort = (value: string) => { this.desiredPort = Number(value); - this.ready = Boolean(this.desiredPort == 0 || this.currentPort !== this.desiredPort); }; startPortForward = async () => { @@ -105,7 +102,7 @@ export class PortForwardDialog extends Component { portForward.forwardPort = desiredPort; port = await addPortForward(portForward); } - + if (dialogState.openInBrowser) { portForward.forwardPort = port; openPortForward(portForward); @@ -120,7 +117,7 @@ export class PortForwardDialog extends Component { renderContents() { return ( <> -
+
Local port to forward from: @@ -134,6 +131,13 @@ export class PortForwardDialog extends Component { onChange={this.changePort} />
+ dialogState.openInBrowser = value} + />
); @@ -162,7 +166,6 @@ export class PortForwardDialog extends Component { contentClass="flex gaps column" next={this.startPortForward} nextLabel={this.currentPort === 0 ? "Start" : "Restart"} - disabledNext={!this.ready} > {this.renderContents()} diff --git a/src/renderer/port-forward/port-forward-item.ts b/src/renderer/port-forward/port-forward-item.ts index 9251b0fcef..e1a4f350db 100644 --- a/src/renderer/port-forward/port-forward-item.ts +++ b/src/renderer/port-forward/port-forward-item.ts @@ -78,6 +78,10 @@ export class PortForwardItem implements ItemObject { getForwardPort() { return this.forwardPort; } + + getStatus() { + return "Active"; // to-do allow port-forward-items to be stopped (without removing them) + } getSearchFields() { return [ diff --git a/src/renderer/port-forward/port-forward.store.ts b/src/renderer/port-forward/port-forward.store.ts index 49ebee61d4..7731d5e087 100644 --- a/src/renderer/port-forward/port-forward.store.ts +++ b/src/renderer/port-forward/port-forward.store.ts @@ -81,6 +81,16 @@ export class PortForwardStore extends ItemStore { async removeSelectedItems() { return Promise.all(this.selectedItems.map(removePortForward)); } + + getById(id: string) { + const index = this.getIndexById(id); + + if (index === -1) { + return null; + } + + return this.getItems()[index]; + } } interface PortForwardResult { diff --git a/src/renderer/utils/__tests__/converted-name.test.ts b/src/renderer/utils/__tests__/converted-name.test.ts new file mode 100644 index 0000000000..35ac991b4b --- /dev/null +++ b/src/renderer/utils/__tests__/converted-name.test.ts @@ -0,0 +1,36 @@ +/** + * 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 { getConvertedParts } from "../name-parts"; + +describe("getConvertedParts", () => { + it.each([ + ["hello", ["hello"]], + ["hello.goodbye", ["hello", "goodbye"]], + ["hello.1", ["hello", 1]], + ["3-hello.1", [3, "hello", 1]], + ["3_hello.1", [3, "hello", 1]], + ["3_hello.1/foobar", [3, "hello", 1, "foobar"]], + ["3_hello.1/foobar\\new", [3, "hello", 1, "foobar", "new"]], + ])("Splits '%s' as into %j", (input, output) => { + expect(getConvertedParts(input)).toEqual(output); + }); +}); diff --git a/src/renderer/utils/createStorage.ts b/src/renderer/utils/createStorage.ts index 2e79e35209..e2ff3dc314 100755 --- a/src/renderer/utils/createStorage.ts +++ b/src/renderer/utils/createStorage.ts @@ -25,7 +25,6 @@ import path from "path"; import { comparer, observable, reaction, toJS, when } from "mobx"; import fse from "fs-extra"; import { StorageHelper } from "./storageHelper"; -import { ClusterStore } from "../../common/cluster-store"; import logger from "../../main/logger"; import { getHostedClusterId, getPath } from "../../common/utils"; import { isTestEnv } from "../../common/vars"; @@ -75,11 +74,6 @@ export function createAppStorage(key: string, defaultValue: T, clusterId?: st equals: comparer.structural, // save only when something really changed }); - // remove json-file when cluster deleted - if (clusterId !== undefined) { - when(() => ClusterStore.getInstance(false)?.removedClusters.has(clusterId)).then(removeFile); - } - async function saveFile(state: Record = {}) { logger.info(`${logPrefix} saving ${filePath}`); @@ -92,11 +86,6 @@ export function createAppStorage(key: string, defaultValue: T, clusterId?: st }); } } - - function removeFile() { - logger.debug(`${logPrefix} removing ${filePath}`); - fse.unlink(filePath).catch(Function); - } } return new StorageHelper(key, { diff --git a/src/renderer/utils/index.ts b/src/renderer/utils/index.ts index 537aab13ba..5c765d4c8a 100755 --- a/src/renderer/utils/index.ts +++ b/src/renderer/utils/index.ts @@ -22,19 +22,18 @@ // Common usage utils & helpers export * from "../../common/utils"; - -export * from "./cssVar"; -export * from "./cssNames"; export * from "../../common/event-emitter"; -export * from "./saveFile"; -export * from "./prevDefault"; -export * from "./storageHelper"; -export * from "./createStorage"; -export * from "./interval"; + export * from "./copyToClipboard"; -export * from "./isReactNode"; -export * from "../../common/utils/convertMemory"; -export * from "../../common/utils/convertCpu"; -export * from "./metricUnitsToNumber"; +export * from "./createStorage"; +export * from "./cssNames"; +export * from "./cssVar"; export * from "./display-booleans"; +export * from "./interval"; export * from "./isMiddleClick"; +export * from "./isReactNode"; +export * from "./metricUnitsToNumber"; +export * from "./name-parts"; +export * from "./prevDefault"; +export * from "./saveFile"; +export * from "./storageHelper"; diff --git a/src/renderer/components/+cluster/cluster-pie-charts.scss b/src/renderer/utils/name-parts.ts similarity index 70% rename from src/renderer/components/+cluster/cluster-pie-charts.scss rename to src/renderer/utils/name-parts.ts index 4f2c392bb7..e60948bc0f 100644 --- a/src/renderer/components/+cluster/cluster-pie-charts.scss +++ b/src/renderer/utils/name-parts.ts @@ -19,32 +19,18 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -.ClusterPieCharts { - background: transparent!important; - padding: 0!important; +/** + * Split `name` into the parts seperated by one or more of (-, _, or .) and + * the sections can be converted to numbers will be converted + * @param name A kube object name + * @returns The converted parts of the name + */ +export function getConvertedParts(name: string): (string | number)[] { + return name + .split(/[-_./\\]+/) + .map(part => { + const converted = +part; - .empty { - background: $contentColor; - min-height: 280px; - text-align: center; - padding: $padding * 2; - } - - .NodeCharts { - margin-bottom: 0; - } - - .chart { - --flex-gap: #{$padding * 2}; - background: $contentColor; - padding: $padding * 2 $padding; - - .chart-title { - margin-bottom: 0; - } - - .legend { - --flex-gap: #{$padding}; - } - } -} \ No newline at end of file + return isNaN(converted) ? part : converted; + }); +} diff --git a/yarn.lock b/yarn.lock index 2ff34dcd92..941d503a8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -330,10 +330,10 @@ resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== -"@cspotcode/source-map-support@0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz#118511f316e2e87ee4294761868e254d3da47960" - integrity sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg== +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== dependencies: "@cspotcode/source-map-consumer" "0.8.0" @@ -386,21 +386,6 @@ dir-compare "^2.4.0" fs-extra "^9.0.1" -"@emeraldpay/hashicon-react@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@emeraldpay/hashicon-react/-/hashicon-react-0.4.0.tgz#a05a1c9af23721a81450978fad816908fa3246b7" - integrity sha512-n+84V9R4g84B62NneYg5mz9Vf1xEBURZoyC4G1K9Rs9C7949D3SxgLhuXYyFKkNO5F5qzms5Qr3LGE5QNEdqJg== - dependencies: - "@emeraldpay/hashicon" "^0.4.0" - react "^16.8.0" - -"@emeraldpay/hashicon@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@emeraldpay/hashicon/-/hashicon-0.4.0.tgz#0dcd4db6d589b4eb30495b9995ff66fc9e57f833" - integrity sha512-2EB6Z8gS/DK36+SslIuoHE8wdU6pBqljMTjIYxLC8hBN8n2yhhE+xdmifPjqfqOvF/7Wb5HmoNaPX6RGuBrasg== - dependencies: - js-sha3 "^0.8.0" - "@emotion/cache@^10.0.27", "@emotion/cache@^10.0.9": version "10.0.29" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" @@ -1262,10 +1247,10 @@ "@babel/runtime" "^7.12.5" "@testing-library/dom" "^7.28.1" -"@testing-library/user-event@^13.2.1": - version "13.2.1" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.2.1.tgz#7a71a39e50b4a733afbe2916fa2b99966e941f98" - integrity sha512-cczlgVl+krjOb3j1625usarNEibI0IFRJrSWX9UsJ1HKYFgCQv9Nb7QAipUDXl3Xdz8NDTsiS78eAkPSxlzTlw== +"@testing-library/user-event@^13.5.0": + version "13.5.0" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295" + integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg== dependencies: "@babel/runtime" "^7.12.5" @@ -1728,10 +1713,10 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@14.17.26", "@types/node@^14.6.2": - version "14.17.26" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.26.tgz#47a53c7e7804490155a4646d60c8e194816d073c" - integrity sha512-eSTNkK/nfmnC7IKpOJZixDgG0W2/eHz1qyFN7o/rwwwIHsVRp+G9nbh4BrQ77kbQ2zPu286AQRxkuRLPcR3gXw== +"@types/node@*", "@types/node@14.17.27", "@types/node@^14.6.2": + version "14.17.27" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.27.tgz#5054610d37bb5f6e21342d0e6d24c494231f3b85" + integrity sha512-94+Ahf9IcaDuJTle/2b+wzvjmutxXAEXU6O81JHblYXUg2BDG+dnBy7VxIPHKAyEEDHzCMQydTJuWvrE+Aanzw== "@types/node@^10.12.0": version "10.17.24" @@ -1820,10 +1805,10 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== -"@types/react-beautiful-dnd@^13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#fb3fe24a334cc757d290e75722e4d3c8368ce3a3" - integrity sha512-1lBBxVSutE8CQM37Jq7KvJwuA94qaEEqsx+G0dnwzG6Sfwf6JGcNeFk5jjjhJli1q2naeMZm+D/dvT/zyX4QPw== +"@types/react-beautiful-dnd@^13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz#510405abb09f493afdfd898bf83995dc6385c130" + integrity sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg== dependencies: "@types/react" "*" @@ -1870,10 +1855,10 @@ "@types/react-dom" "*" "@types/react-transition-group" "*" -"@types/react-table@^7.7.6": - version "7.7.6" - resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.6.tgz#4899ccc46b4a1de08cc1daf120842e37e112e51e" - integrity sha512-ZMFHh1sG5AGDmhVRpz9mgGByGmBFAqnZ7QnyqGa5iAlKtcSC3vb/gul47lM0kJ1uvlawc+qN5k+++pe+GBdJ+g== +"@types/react-table@^7.7.7": + version "7.7.7" + resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.7.tgz#503be1ce2e06857c11b281629f539358d32671f9" + integrity sha512-3l2TP4detx9n5Jt44XhdH7Ku6PYwz6kB83P8W+YcBMUkIHtiw2gsCCcq9c4wyCIcdSwcTlWZ9WqH4PF7Yfbprg== dependencies: "@types/react" "*" @@ -2106,10 +2091,10 @@ "@types/webpack" "^4" http-proxy-middleware "^1.0.0" -"@types/webpack-env@^1.16.2": - version "1.16.2" - resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.2.tgz#8db514b059c1b2ae14ce9d7bb325296de6a9a0fa" - integrity sha512-vKx7WNQNZDyJveYcHAm9ZxhqSGLYwoyLhrHjLBOkw3a7cT76sTdjgtwyijhk1MaHyRIuSztcVwrUOO/NEu68Dw== +"@types/webpack-env@^1.16.3": + version "1.16.3" + resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a" + integrity sha512-9gtOPPkfyNoEqCQgx4qJKkuNm/x0R2hKR7fdl7zvTJyHnIisuE/LfvXOsYWL0o3qq6uiBnKZNNNzi3l0y/X+xw== "@types/webpack-node-externals@^1.7.1": version "1.7.1" @@ -2179,7 +2164,7 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^4.33.0", "@typescript-eslint/eslint-plugin@^4.5.0": +"@typescript-eslint/eslint-plugin@^4.33.0": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" integrity sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg== @@ -5465,12 +5450,67 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -esbuild-loader@^2.15.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/esbuild-loader/-/esbuild-loader-2.15.1.tgz#5a3940f5d20317f1a35720efa33e933f97c923e9" - integrity sha512-JRBL6uTeWplMbylNBt9gxLKMjD8wKnqGq786QV/cm/nPBSNA9/kC7/vNwCXTDPfYqHoWsjyfH7ub9ekN0kdAYQ== +esbuild-android-arm64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.8.tgz#c20e875c3c98164b1ffba9b28637bdf96f5e9e7c" + integrity sha512-AilbChndywpk7CdKkNSZ9klxl+9MboLctXd9LwLo3b0dawmOF/i/t2U5d8LM6SbT1Xw36F8yngSUPrd8yPs2RA== + +esbuild-darwin-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.8.tgz#f46e6b471ddbf62265234808a6a1aa91df18a417" + integrity sha512-b6sdiT84zV5LVaoF+UoMVGJzR/iE2vNUfUDfFQGrm4LBwM/PWXweKpuu6RD9mcyCq18cLxkP6w/LD/w9DtX3ng== + +esbuild-darwin-arm64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.8.tgz#a991157a6013facd4f2e14159b7da52626c90154" + integrity sha512-R8YuPiiJayuJJRUBG4H0VwkEKo6AvhJs2m7Tl0JaIer3u1FHHXwGhMxjJDmK+kXwTFPriSysPvcobXC/UrrZCQ== + +esbuild-freebsd-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.8.tgz#301601d2e443ad458960e359b402a17d9500be9d" + integrity sha512-zBn6urrn8FnKC+YSgDxdof9jhPCeU8kR/qaamlV4gI8R3KUaUK162WYM7UyFVAlj9N0MyD3AtB+hltzu4cysTw== + +esbuild-freebsd-arm64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.8.tgz#039a63acc12ec0892006c147ea221e55f9125a9f" + integrity sha512-pWW2slN7lGlkx0MOEBoUGwRX5UgSCLq3dy2c8RIOpiHtA87xAUpDBvZK10MykbT+aMfXc0NI2lu1X+6kI34xng== + +esbuild-linux-32@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.8.tgz#c537b67d7e694b60bfa2786581412838c6ba0284" + integrity sha512-T0I0ueeKVO/Is0CAeSEOG9s2jeNNb8jrrMwG9QBIm3UU18MRB60ERgkS2uV3fZ1vP2F8i3Z2e3Zju4lg9dhVmw== + +esbuild-linux-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.8.tgz#0092fc8a064001a777bfa0e3b425bb8be8f96e6a" + integrity sha512-Bm8SYmFtvfDCIu9sjKppFXzRXn2BVpuCinU1ChTuMtdKI/7aPpXIrkqBNOgPTOQO9AylJJc1Zw6EvtKORhn64w== + +esbuild-linux-arm64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.8.tgz#5cd3f2bb924212971482e8dbc25c4afd09b28110" + integrity sha512-X4pWZ+SL+FJ09chWFgRNO3F+YtvAQRcWh0uxKqZSWKiWodAB20flsW/OWFYLXBKiVCTeoGMvENZS/GeVac7+tQ== + +esbuild-linux-arm@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.8.tgz#ad634f96bf2975536907aeb9fdb75a3194f4ddce" + integrity sha512-4/HfcC40LJ4GPyboHA+db0jpFarTB628D1ifU+/5bunIgY+t6mHkJWyxWxAAE8wl/ZIuRYB9RJFdYpu1AXGPdg== + +esbuild-linux-mips64le@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.8.tgz#57857edfebf9bf65766dc8be1637f2179c990572" + integrity sha512-o7e0D+sqHKT31v+mwFircJFjwSKVd2nbkHEn4l9xQ1hLR+Bv8rnt3HqlblY3+sBdlrOTGSwz0ReROlKUMJyldA== + +esbuild-linux-ppc64le@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.8.tgz#fdb82a059a5b86bb10fb42091b4ebcf488b9cd46" + integrity sha512-eZSQ0ERsWkukJp2px/UWJHVNuy0lMoz/HZcRWAbB6reoaBw7S9vMzYNUnflfL3XA6WDs+dZn3ekHE4Y2uWLGig== + +esbuild-loader@^2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/esbuild-loader/-/esbuild-loader-2.16.0.tgz#a44a57a77ed2810d6b278579271f77d739aa7bc9" + integrity sha512-LCJEwkf+nMJbNmVYNgg/0PaIZDdr5OcHw1qbWAZLkrmBRX+KwHY/yAS6ia98UBtwzk/WhsftUBNB6tfPHgFIxw== dependencies: - esbuild "^0.12.21" + esbuild "^0.13.4" joycon "^3.0.1" json5 "^2.2.0" loader-utils "^2.0.0" @@ -5478,10 +5518,58 @@ esbuild-loader@^2.15.1: type-fest "^1.4.0" webpack-sources "^2.2.0" -esbuild@^0.12.21, esbuild@^0.12.24: - version "0.12.29" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.29.tgz#be602db7c4dc78944a9dbde0d1ea19d36c1f882d" - integrity sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g== +esbuild-netbsd-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.8.tgz#d7879e7123d3b2c04754ece8bd061aa6866deeff" + integrity sha512-gZX4kP7gVvOrvX0ZwgHmbuHczQUwqYppxqtoyC7VNd80t5nBHOFXVhWo2Ad/Lms0E8b+wwgI/WjZFTCpUHOg9Q== + +esbuild-openbsd-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.8.tgz#88b280b6cb0a3f6adb60abf27fc506c506a35cf0" + integrity sha512-afzza308X4WmcebexbTzAgfEWt9MUkdTvwIa8xOu4CM2qGbl2LanqEl8/LUs8jh6Gqw6WsicEK52GPrS9wvkcw== + +esbuild-sunos-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.8.tgz#229ae7c7703196a58acd0f0291ad9bebda815d63" + integrity sha512-mWPZibmBbuMKD+LDN23LGcOZ2EawMYBONMXXHmbuxeT0XxCNwadbCVwUQ/2p5Dp5Kvf6mhrlIffcnWOiCBpiVw== + +esbuild-windows-32@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.8.tgz#892d093e32a21c0c9135e5a0ffdc380aeb70e763" + integrity sha512-QsZ1HnWIcnIEApETZWw8HlOhDSWqdZX2SylU7IzGxOYyVcX7QI06ety/aDcn437mwyO7Ph4RrbhB+2ntM8kX8A== + +esbuild-windows-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.8.tgz#7defd8d79ae3bb7e6f53b65a7190be7daf901686" + integrity sha512-76Fb57B9eE/JmJi1QmUW0tRLQZfGo0it+JeYoCDTSlbTn7LV44ecOHIMJSSgZADUtRMWT9z0Kz186bnaB3amSg== + +esbuild-windows-arm64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.8.tgz#e59ae004496fd8a5ab67bfc7945a2e47480d6fb9" + integrity sha512-HW6Mtq5eTudllxY2YgT62MrVcn7oq2o8TAoAvDUhyiEmRmDY8tPwAhb1vxw5/cdkbukM3KdMYtksnUhF/ekWeg== + +esbuild@^0.13.4, esbuild@^0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.8.tgz#bd7cc51b881ab067789f88e17baca74724c1ec4f" + integrity sha512-A4af7G7YZLfG5OnARJRMtlpEsCkq/zHZQXewgPA864l9D6VjjbH1SuFYK/OSV6BtHwDGkdwyRrX0qQFLnMfUcw== + optionalDependencies: + esbuild-android-arm64 "0.13.8" + esbuild-darwin-64 "0.13.8" + esbuild-darwin-arm64 "0.13.8" + esbuild-freebsd-64 "0.13.8" + esbuild-freebsd-arm64 "0.13.8" + esbuild-linux-32 "0.13.8" + esbuild-linux-64 "0.13.8" + esbuild-linux-arm "0.13.8" + esbuild-linux-arm64 "0.13.8" + esbuild-linux-mips64le "0.13.8" + esbuild-linux-ppc64le "0.13.8" + esbuild-netbsd-64 "0.13.8" + esbuild-openbsd-64 "0.13.8" + esbuild-sunos-64 "0.13.8" + esbuild-windows-32 "0.13.8" + esbuild-windows-64 "0.13.8" + esbuild-windows-arm64 "0.13.8" escalade@^3.1.1: version "3.1.1" @@ -5567,16 +5655,12 @@ eslint-plugin-react@^7.26.1: semver "^6.3.0" string.prototype.matchall "^4.0.5" -eslint-plugin-unused-imports@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-1.0.1.tgz#f95f48637bb166ed924ecb5ebf27761a57da4178" - integrity sha512-AVDcgeoZZoBH/g5743nvWQK7/V7w2RMILHvogfBnYa1s47los7G2ysEweRx0yJ8pSVnITJvxTBkefQbJowTi3w== +eslint-plugin-unused-imports@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-1.1.5.tgz#a2b992ef0faf6c6c75c3815cc47bde76739513c2" + integrity sha512-TeV8l8zkLQrq9LBeYFCQmYVIXMjfHgdRQLw7dEZp4ZB3PeR10Y5Uif11heCsHRmhdRIYMoewr1d9ouUHLbLHew== dependencies: - "@typescript-eslint/eslint-plugin" "^4.5.0" - eslint "^7.11.0" eslint-rule-composer "^0.3.0" - requireindex "~1.2.0" - typescript "^4.0.3" eslint-rule-composer@^0.3.0: version "0.3.0" @@ -5623,7 +5707,7 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint@^7.11.0, eslint@^7.32.0: +eslint@^7.32.0: version "7.32.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== @@ -8432,11 +8516,6 @@ jpeg-js@^0.4.2: resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b" integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q== -js-sha3@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" - integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -9317,10 +9396,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.3.tgz#3551c4958c4da36897bda2a16812ef1399c8d6b0" - integrity sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA== +marked@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753" + integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA== marked@^3.0.4: version "3.0.6" @@ -9849,11 +9928,18 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" -node-fetch@2.6.1, node-fetch@^2.6.1: +node-fetch@2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@^2.6.5: + version "2.6.5" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd" + integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ== + dependencies: + whatwg-url "^5.0.0" + node-forge@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" @@ -9958,10 +10044,10 @@ node-pty@^0.10.1: dependencies: nan "^2.14.0" -nodemon@^2.0.13: - version "2.0.13" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.13.tgz#67d40d3a4d5bd840aa785c56587269cfcf5d24aa" - integrity sha512-UMXMpsZsv1UXUttCn6gv8eQPhn6DR4BW+txnL3IN5IHqrCwcrT/yWHfL35UsClGXknTH79r5xbu+6J1zNHuSyA== +nodemon@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.14.tgz#287c7a2f6cd8a18b07e94cd776ecb6a82e4ba439" + integrity sha512-frcpDx+PviKEQRSYzwhckuO2zoHcBYLHI754RE9z5h1RGtrngerc04mLpQQCPWBkH/2ObrX7We9YiwVSYZpFJQ== dependencies: chokidar "^3.2.2" debug "^3.2.6" @@ -11677,15 +11763,6 @@ react-window@^1.8.5: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" -react@^16.8.0: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" - integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" @@ -12042,11 +12119,6 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -requireindex@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef" - integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww== - requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -13649,6 +13721,11 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" @@ -13698,12 +13775,12 @@ ts-loader@^7.0.5: micromatch "^4.0.0" semver "^6.0.0" -ts-node@^10.2.1: - version "10.2.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.2.1.tgz#4cc93bea0a7aba2179497e65bb08ddfc198b3ab5" - integrity sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw== +ts-node@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.3.0.tgz#a797f2ed3ff50c9a5d814ce400437cb0c1c048b4" + integrity sha512-RYIy3i8IgpFH45AX4fQHExrT8BxDeKTdC83QFJkNzkvt8uFB6QJ8XMyhynYiKMLxt9a7yuXaDBZNOYS3XjDcYw== dependencies: - "@cspotcode/source-map-support" "0.6.1" + "@cspotcode/source-map-support" "0.7.0" "@tsconfig/node10" "^1.0.7" "@tsconfig/node12" "^1.0.7" "@tsconfig/node14" "^1.0.0" @@ -13886,7 +13963,7 @@ typescript-plugin-css-modules@^3.4.0: stylus "^0.54.8" tsconfig-paths "^3.9.0" -typescript@^4.0.3, typescript@^4.4.3: +typescript@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324" integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA== @@ -14290,6 +14367,11 @@ wcwidth@^1.0.0: dependencies: defaults "^1.0.3" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -14456,6 +14538,14 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^8.0.0, whatwg-url@^8.5.0: version "8.7.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77"