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

Merge remote-tracking branch 'origin/master' into monaco_editor_refactoring

# Conflicts:
#	package.json
This commit is contained in:
Roman 2021-10-22 16:38:11 +03:00
commit 40f0dc989d
81 changed files with 1630 additions and 802 deletions

View File

@ -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)

View File

@ -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 <version-type>` where `<version-type>` 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 <version-type> --git-tag-version false` where `<version-type>` is one of the following:
- `major`
- `minor`
- `patch`
- `premajor`
- `preminor`
- `prepatch`
- `premajor` [--preid=<prerelease-id>]
- `preminor` [--preid=<prerelease-id>]
- `prepatch` [--preid=<prerelease-id>]
- `prerelease [--preid=<prerelease-id>]`
where `<prerelease-id>` 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:

View File

@ -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}'`);

View File

@ -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",

View File

@ -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<KubernetesClusterMetadata,
context.menuItems.push({
title: "Settings",
icon: "edit",
onClick: () => context.navigate(`/entity/${this.metadata.uid}/settings`)
onClick: () => broadcastMessage(
IpcRendererNavigationEvents.NAVIGATE_IN_APP,
`/entity/${this.metadata.uid}/settings`,
),
});
}

View File

@ -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[];
}

View File

@ -40,7 +40,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
private static StateChannel = "cluster:state";
clusters = observable.map<ClusterId, Cluster>();
removedClusters = observable.map<ClusterId, Cluster>();
protected disposer = disposer();
@ -144,7 +143,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
protected fromStore({ clusters = [] }: ClusterStoreModel = {}) {
const currentClusters = new Map(this.clusters);
const newClusters = new Map<ClusterId, Cluster>();
const removedClusters = new Map<ClusterId, Cluster>();
// update new clusters
for (const clusterModel of clusters) {
@ -162,15 +160,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
}
// 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 {

View File

@ -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<string, string>;
}
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<string, string>;
constructor(data: SecretData) {
super(data);
autoBind(this);

View File

@ -52,6 +52,7 @@ export interface JsonApiConfig {
apiBase: string;
serverAddress: string;
debug?: boolean;
getRequestOptions?: () => Promise<RequestInit>;
}
export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
static reqInitDefault: RequestInit = {
@ -68,18 +69,26 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
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<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
return this.request<T>(path, params, { ...reqInit, method: "get" });
}
getResponse(path: string, params?: P, init: RequestInit = {}): Promise<Response> {
async getResponse(path: string, params?: P, init: RequestInit = {}): Promise<Response> {
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<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
protected async request<D>(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) {

View File

@ -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<string>);
clientCertificateData?: string;
clientKeyData?: string;
}
@ -135,12 +136,6 @@ export function forCluster<T extends KubeObject>(cluster: ILocalKubeApiConfig, k
export function forRemoteCluster<T extends KubeObject>(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor<T>): KubeApi<T> {
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<T extends KubeObject>(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({

View File

@ -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<T extends KubeObject> extends ItemStore<T>
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);

View File

@ -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);

View File

@ -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<PortForwardsRouteParams>(portForwardsRoute.path);

View File

@ -135,12 +135,32 @@ const allowErrorReporting: PreferenceDescription<boolean> = {
},
};
export interface DownloadMirror {
url: string;
label: string;
platforms: Set<NodeJS.Platform>;
}
export const defaultPackageMirror = "default";
export const packageMirrors = new Map<string, DownloadMirror>([
[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<string> = {
fromStore(val) {
return val ?? "default";
return packageMirrors.has(val) ? val : defaultPackageMirror;
},
toStore(val) {
if (!val || val === "default") {
if (!val || val === defaultPackageMirror) {
return undefined;
}

44
src/common/utils/array.ts Normal file
View File

@ -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<T, N extends number> = N extends N ? number extends N ? T[] : _TupleOf<T, N, []> : never;
type _TupleOf<T, N extends number, R extends unknown[]> = R["length"] extends N ? R : _TupleOf<T, N, [T, ...R]>;
/**
*
* @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<T, N extends number>(...sources: Tuple<T[], N>): Iterator<Tuple<T, N>, Tuple<T[], N>> {
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<T[], N>;
}
for (let i = 0; i < maxSafeLength; i += 1) {
yield sources.map(source => source[i]) as Tuple<T, N>;
}
return sources.map(source => source.slice(maxSafeLength)) as Tuple<T[], N>;
}

View File

@ -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 };

View File

@ -185,3 +185,19 @@ export function reduce<T, R = T>(src: Iterable<T>, reducer: (acc: R, cur: T) =>
export function join(src: Iterable<string>, 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<T>(src: Iterable<T>, fn: (val: T) => any): boolean {
for (const val of src) {
if (!fn(val)) {
return false;
}
}
return true;
}

View File

@ -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<T>(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<T>(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));
}

View File

@ -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);
});

View File

@ -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<string, Buffer>();
static #cache = new Map<string, ChartCacheEntry>();
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<RepoHelmChartList> {
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),
)
);
}

View File

@ -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();

View File

@ -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) => {

View File

@ -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<string, string> = new Map([
@ -51,10 +52,6 @@ const kubectlMap: Map<string, string> = new Map([
["1.20", "1.20.8"],
["1.21", bundledVersion]
]);
const packageMirrors: Map<string, string> = 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);
}
}

View File

@ -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(),

View File

@ -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;

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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<T extends CatalogEntity> {
item: CatalogEntityItem<T> | null | undefined;
@ -57,8 +58,8 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
return (
<>
{showDetails && (
<div className="flex CatalogEntityDetails">
<div className="EntityIcon box top left">
<div className="flex">
<div className={styles.entityIcon}>
<HotbarIcon
uid={item.id}
title={item.name}
@ -70,14 +71,15 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
onClick={() => item.onRun()}
size={128}
data-testid="detail-panel-hot-bar-icon"
className={styles.avatar}
/>
{item?.enabled && (
<div className="IconHint">
<div className={styles.hint}>
Click to open
</div>
)}
</div>
<div className="box grow EntityMetadata">
<div className={cssNames("box grow", styles.metadata)}>
<DrawerItem name="Name">
{item.name}
</DrawerItem>
@ -114,7 +116,7 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
return (
<Drawer
className="CatalogEntityDetails"
className={styles.entityDetails}
usePortal={true}
open={true}
title={title}

View File

@ -98,8 +98,8 @@ export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Comp
<HotbarToggleMenuItem
key="hotbar-toggle"
entity={entity}
addContent={<Icon material="push_pin" small tooltip={"Add to Hotbar"}/>}
removeContent={<Icon svg="unpin" small tooltip={"Remove from Hotbar"}/>}
addContent={<Icon material="push_pin" small tooltip="Add to Hotbar"/>}
removeContent={<Icon svg="push_off" small tooltip="Remove from Hotbar"/>}
/>
);

View File

@ -220,7 +220,7 @@ export class Catalog extends React.Component<Props> {
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))}
/>

View File

@ -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));

View File

@ -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;
}

View File

@ -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<Props> {
selected={selfLink === kubeSelectedUrlParam.get()}
onClick={prevDefault(() => toggleDetails(selfLink))}
>
<TableCell className="message">
<TableCell className={styles.message}>
{message}
</TableCell>
<TableCell className="object">
<TableCell className={styles.object}>
{getName()}
</TableCell>
<TableCell className="kind">
@ -148,8 +148,8 @@ export class ClusterIssues extends React.Component<Props> {
if (!warnings.length) {
return (
<div className="no-issues flex column box grow gaps align-center justify-center">
<div><Icon material="check" big sticker/></div>
<div className={cssNames(styles.noIssues, "flex column box grow gaps align-center justify-center")}>
<div><Icon className={styles.allGood} material="check" big sticker/></div>
<div className="ok-title">No issues found</div>
<span>Everything is fine in the Cluster</span>
</div>
@ -158,7 +158,7 @@ export class ClusterIssues extends React.Component<Props> {
return (
<>
<SubHeader>
<SubHeader className={styles.SubHeader}>
<Icon material="error_outline"/>{" "}
<>Warnings: {warnings.length}</>
</SubHeader>
@ -186,7 +186,7 @@ export class ClusterIssues extends React.Component<Props> {
render() {
return (
<div className={cssNames("ClusterIssues flex column", this.props.className)}>
<div className={cssNames(styles.ClusterIssues, "flex column", this.props.className)}>
{this.renderContent()}
</div>
);

View File

@ -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 (
<div className="ClusterMetricSwitchers flex gaps">
<div className="flex gaps" style={{ marginBottom: "calc(var(--margin) * 2)" }}>
<div className="box grow">
<RadioGroup
asButtons
@ -48,7 +46,7 @@ export const ClusterMetricSwitchers = observer(() => {
<Radio label="Worker" value={MetricNodeRole.WORKER}/>
</RadioGroup>
</div>
<div className="box grow metric-switch">
<div className="box grow" style={{ textAlign: "right" }}>
<RadioGroup
asButtons
className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })}

View File

@ -19,12 +19,14 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.ClusterMetrics {
.ClusterMetrics {
position: relative;
min-height: 280px;
padding: calc(var(--margin) * 2);
background: var(--contentColor);
.Chart {
.chart-container {
.chart {
:global(.chart-container) {
width: 100%;
height: 100%;
}

View File

@ -19,14 +19,14 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./cluster-metrics.scss";
import styles from "./cluster-metrics.module.css";
import React from "react";
import { observer } from "mobx-react";
import type { ChartOptions, ChartPoint } from "chart.js";
import { clusterOverviewStore, MetricType } from "./cluster-overview.store";
import { BarChart } from "../chart";
import { bytesToUnits } from "../../utils";
import { bytesToUnits, cssNames } from "../../utils";
import { Spinner } from "../spinner";
import { ZebraStripes } from "../chart/zebra-stripes.plugin";
import { ClusterNoMetrics } from "./cluster-no-metrics";
@ -95,7 +95,7 @@ export const ClusterMetrics = observer(() => {
}
if (!memoryCapacity || !cpuCapacity) {
return <ClusterNoMetrics className="empty"/>;
return <ClusterNoMetrics className={styles.empty}/>;
}
return (
@ -106,12 +106,13 @@ export const ClusterMetrics = observer(() => {
timeLabelStep={5}
showLegend={false}
plugins={[ZebraStripes]}
className={styles.chart}
/>
);
};
return (
<div className="ClusterMetrics flex column">
<div className={cssNames(styles.ClusterMetrics, "flex column")}>
<ClusterMetricSwitchers/>
{renderMetrics()}
</div>

View File

@ -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;
}
}

View File

@ -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 (
<TabLayout>
<div className="ClusterOverview">
<div className={styles.ClusterOverview} data-testid="cluster-overview-page">
{this.renderClusterOverview(isLoaded, isMetricHidden)}
</div>
</TabLayout>

View File

@ -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);
}

View File

@ -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 (
<div className="NodeCharts flex justify-center box grow gaps">
<div className="chart flex column align-center box grow">
<div className="flex justify-center box grow gaps">
<div className={cssNames(styles.chart, "flex column align-center box grow")}>
<PieChart
data={cpuData}
title="CPU"
@ -188,7 +188,7 @@ export const ClusterPieCharts = observer(() => {
/>
{cpuLimitsOverload && renderLimitWarning()}
</div>
<div className="chart flex column align-center box grow">
<div className={cssNames(styles.chart, "flex column align-center box grow")}>
<PieChart
data={memoryData}
title="Memory"
@ -202,7 +202,7 @@ export const ClusterPieCharts = observer(() => {
/>
{memoryLimitsOverload && renderLimitWarning()}
</div>
<div className="chart flex column align-center box grow">
<div className={cssNames(styles.chart, "flex column align-center box grow")}>
<PieChart
data={podsData}
title="Pods"
@ -220,7 +220,7 @@ export const ClusterPieCharts = observer(() => {
if (!nodes.length) {
return (
<div className="empty flex column box grow align-center justify-center">
<div className={cssNames(styles.empty, "flex column box grow align-center justify-center")}>
<Icon material="info"/>
No Nodes Available.
</div>
@ -237,14 +237,14 @@ export const ClusterPieCharts = observer(() => {
const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterOverviewStore.metrics);
if (!memoryCapacity || !cpuCapacity || !podCapacity) {
return <ClusterNoMetrics className="empty"/>;
return <ClusterNoMetrics className={styles.empty}/>;
}
return renderCharts();
};
return (
<div className="ClusterPieCharts flex">
<div className="flex">
{renderContent()}
</div>
);

View File

@ -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(<SecretDetails object={secret}/>);
expect(result.getByTestId("foobar-secret-entry").querySelector(".Icon")).toBeDefined();
});
});

View File

@ -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<Secret> {
export class SecretDetails extends React.Component<Props> {
@observable isSaving = false;
@observable data: { [name: string]: string } = {};
@observable revealSecret: { [name: string]: boolean } = {};
revealSecret = new ObservableToggleSet<string>();
constructor(props: Props) {
super(props);
@ -58,7 +57,7 @@ export class SecretDetails extends React.Component<Props> {
if (secret) {
this.data = secret.data;
this.revealSecret = {};
this.revealSecret.clear();
}
})
]);
@ -82,6 +81,69 @@ export class SecretDetails extends React.Component<Props> {
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 (
<div key={name} className="data" data-testid={`${name}-secret-entry`}>
<div className="name">{name}</div>
<div className="flex gaps align-center">
<Input
multiLine
theme="round-black"
className="box grow"
value={value || ""}
onChange={value => this.editData(name, value, !revealSecret)}
/>
{typeof decodedVal === "string" && (
<Icon
material={revealSecret ? "visibility" : "visibility_off"}
tooltip={revealSecret ? "Hide" : "Show"}
onClick={() => this.revealSecret.toggle(name)}
/>
)}
</div>
</div>
);
};
renderData() {
const secrets = Object.entries(this.data);
if (secrets.length === 0) {
return null;
}
return (
<>
<DrawerTitle title="Data" />
{secrets.map(this.renderSecret)}
<Button
primary
label="Save" waiting={this.isSaving}
className="save-btn"
onClick={this.saveSecret}
/>
</>
);
}
render() {
const { object: secret } = this.props;
@ -101,52 +163,7 @@ export class SecretDetails extends React.Component<Props> {
<DrawerItem name="Type">
{secret.type}
</DrawerItem>
{!isEmpty(this.data) && (
<>
<DrawerTitle title="Data"/>
{
Object.entries(this.data).map(([name, value]) => {
const revealSecret = this.revealSecret[name];
let decodedVal = "";
try {
decodedVal = base64.decode(value);
} catch {
decodedVal = "";
}
value = revealSecret ? decodedVal : value;
return (
<div key={name} className="data">
<div className="name">{name}</div>
<div className="flex gaps align-center">
<Input
multiLine
theme="round-black"
className="box grow"
value={value || ""}
onChange={value => this.editData(name, value, !revealSecret)}
/>
{decodedVal && (
<Icon
material={`visibility${revealSecret ? "" : "_off"}`}
tooltip={revealSecret ? "Hide" : "Show"}
onClick={() => this.revealSecret[name] = !revealSecret}
/>)
}
</div>
</div>
);
})
}
<Button
primary
label="Save" waiting={this.isSaving}
className="save-btn"
onClick={this.saveSecret}
/>
</>
)}
{this.renderData()}
</div>
);
}

View File

@ -89,11 +89,11 @@ export class CrdResources extends React.Component<Props> {
searchFilters={[
item => item.getSearchFields(),
]}
renderHeaderTitle={crd.getResourceTitle()}
renderHeaderTitle={crd.getResourceKind()}
customizeHeader={({ searchProps, ...headerPlaceholders }) => ({
searchProps: {
...searchProps,
placeholder: `Search ${crd.getResourceTitle()}...`,
placeholder: `${crd.getResourceKind()} search ...`,
},
...headerPlaceholders
})}

View File

@ -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;
}
}

View File

@ -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<Props> {
renderResourceName() {
const { portForward } = this.props;
const name = portForward.getName();
const api = {
"service": serviceApi,
"pod": podsApi
}[portForward.kind];
if (!api) {
return (
<span>{name}</span>
);
}
return (
<Link to={getDetailsUrl(api.getUrl({ name, namespace: portForward.getNs() }))}>
{name}
</Link>
);
}
renderContent() {
const { portForward } = this.props;
if (!portForward) return null;
return (
<div>
<DrawerItem name="Resource Name">
{this.renderResourceName()}
</DrawerItem>
<DrawerItem name="Namespace">
{portForward.getNs()}
</DrawerItem>
<DrawerItem name="Kind">
{portForward.getKind()}
</DrawerItem>
<DrawerItem name="Pod Port">
{portForward.getPort()}
</DrawerItem>
<DrawerItem name="Local Port">
{portForward.getForwardPort()}
</DrawerItem>
<DrawerItem name="Status">
<span className={cssNames("status", portForward.getStatus().toLowerCase())}>{portForward.getStatus()}</span>
</DrawerItem>
</div>
);
}
render() {
const { hideDetails, portForward } = this.props;
const toolbar = <PortForwardMenu portForward={portForward} toolbar hideDetails={hideDetails}/>;
return (
<Drawer
className="PortForwardDetails"
usePortal={true}
open={!!portForward}
title="Port Forward"
onClose={hideDetails}
toolbar={toolbar}
>
{this.renderContent()}
</Drawer>
);
}
}

View File

@ -24,5 +24,10 @@
&.warning {
@include table-cell-warning;
}
&.status {
@include port-forward-status-colors;
flex: 0.6;
}
}
}

View File

@ -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<PortForwardsRouteParams> {
}
@observer
export class PortForwards extends React.Component {
export class PortForwards extends React.Component<Props> {
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 => (
<PortForwardMenu
@ -96,7 +133,15 @@ export class PortForwards extends React.Component {
customizeRemoveDialog={selectedItems => ({
message: this.renderRemoveDialogMessage(selectedItems)
})}
detailsItem={this.selectedPortForward}
onDetails={this.onDetails}
/>
{this.selectedPortForward && (
<PortForwardDetails
portForward={this.selectedPortForward}
hideDetails={this.hideDetails}
/>
)}
</>
);
}

View File

@ -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;
}
}
}

View File

@ -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<string>[] = [
{ value: "default", label: "Default (Google)" },
{ value: "china", label: "China (Azure)" },
];
const downloadMirrorOptions: SelectOption<string>[] = 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"
/>
</section>

View File

@ -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<Props> {
uid: null,
selfLink: null,
resourceVersion: null
}
},
type: SecretType.Opaque
});
}

View File

@ -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<Props> {
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(),

View File

@ -71,11 +71,11 @@ export class DeploymentReplicaSets extends React.Component<Props> {
<DrawerTitle title="Deploy Revisions"/>
<Table
selectable
tableId="deployment_replica_sets_view"
scrollable={false}
sortable={this.sortingCallbacks}
sortByDefault={{ sortBy: sortBy.pods, orderBy: "desc" }}
sortSyncWithUrl={false}
tableId="deployment_replica_sets_view"
className="box grow"
>
<TableHead>

View File

@ -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<Props> {
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(),

View File

@ -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<Props> {
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(),

View File

@ -51,26 +51,14 @@ interface Props extends OptionalProps {
interface OptionalProps {
maxCpu?: number;
maxMemory?: number;
showTitle?: boolean;
}
@observer
export class PodDetailsList extends React.Component<Props> {
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<Props> {
}
render() {
const { pods, showTitle } = this.props;
const virtual = pods.length > 100;
const { pods } = this.props;
if (!pods.length && !podsStore.isLoaded) return (
<div className="PodDetailsList flex justify-center"><Spinner/></div>
);
if (!pods.length) return null;
if (!podsStore.isLoaded) {
return (
<div className="PodDetailsList flex justify-center">
<Spinner />
</div>
);
}
if (!pods.length) {
return null;
}
const virtual = pods.length > 20;
return (
<div className="PodDetailsList flex column">
{showTitle && <DrawerTitle title="Pods"/>}
<DrawerTitle title="Pods" />
<Table
tableId="workloads_pod_details_list"
items={pods}
selectable
virtual={virtual}
scrollable={false}
sortable={this.sortingCallbacks}
virtual={virtual}
// 660 is the exact hight required for 20 items with the default paddings
virtualHeight={660}
sortable={{
[sortBy.name]: pod => 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"
>
<TableHead>
@ -176,9 +180,6 @@ export class PodDetailsList extends React.Component<Props> {
<TableCell className="memory" sortBy={sortBy.memory}>Memory</TableCell>
<TableCell className="status">Status</TableCell>
</TableHead>
{
!virtual && pods.map(pod => this.getTableRow(pod.getId()))
}
</Table>
</div>
);

View File

@ -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<Props> {
return (
<KubeObjectListLayout
className="Pods" store={podsStore}
dependentStores={[volumeClaimStore, eventStore]}
dependentStores={[eventStore]} // status icon component uses event store
tableId = "workloads_pods"
isConfigurable
sortingCallbacks={{
[columnId.name]: pod => pod.getName(),
[columnId.name]: pod => getConvertedParts(pod.getName()),
[columnId.namespace]: pod => pod.getNs(),
[columnId.containers]: pod => pod.getContainers().length,
[columnId.restarts]: pod => pod.getRestartsCount(),

View File

@ -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<Props> {
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(),

View File

@ -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<Props> {
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(),

View File

@ -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 {
<ReplicaSetScaleDialog/>
<CronJobTriggerDialog/>
<PortForwardDialog/>
<DeleteClusterDialog/>
<CommandContainer clusterId={App.clusterId}/>
</ErrorBoundary>
</Router>

View File

@ -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) {

View File

@ -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);
}

View File

@ -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,

View File

@ -44,20 +44,16 @@ interface Props extends DOMAttributes<HTMLElement> {
@observer
export class HotbarEntityIcon extends React.Component<Props> {
@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);

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L12.8,16.07V22H11.2V16H6V14L8,12V11.27L2,5.27M16,12L18,14V16H17.82L8,6.18V4H7V2H17V4H16V12Z" /></svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<path d="M12.6,12.1V2H7C6.5,2,6,2.5,6,3s0.5,1,1,1h1v5c0,1.7-1.3,3-3,3v2h6v7l1,1l1-1v-7h6v-1.9H12.6z"/>
<polygon points="23.6,3.7 21.9,2 19.4,4.5 16.9,2 15.2,3.7 17.7,6.2 15.2,8.7 16.9,10.4 19.4,7.9 21.9,10.4 23.6,8.7 21.1,6.2 "/>
</svg>

Before

Width:  |  Height:  |  Size: 487 B

View File

@ -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<Props> {
};
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<Props> {
placement={ResizeSide.TRAILING}
growthDirection={ResizeGrowthDirection.LEFT_TO_RIGHT}
getCurrentExtent={() => sidebarWidth}
onDrag={onSidebarResize}
onDrag={this.onSidebarResize}
onDoubleClick={() => this.onSidebarResize(defaultSidebarWidth)}
minExtent={120}
maxExtent={400}
/>

View File

@ -28,7 +28,9 @@ export interface SidebarStorageState {
}
}
export const defaultSidebarWidth = 200;
export const sidebarStorage = createStorage<SidebarStorageState>("sidebar", {
width: 200,
width: defaultSidebarWidth,
expanded: {},
});

View File

@ -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<Props> {
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<Props> {
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);
}}
/>
<div className={styles.clusterName}>
{metadata.name}

View File

@ -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);
});
});
});
});

View File

@ -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<T>(rawItems: T[], sortingCallback: TableSortCallback<T> | 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;
}

View File

@ -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<Item> extends React.DOMAttributes<HTMLDivElement> {
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<TableRowProps>;
renderRow?: (item: Item) => React.ReactElement<TableRowProps>;
@ -78,9 +95,10 @@ export class Table<Item> extends React.Component<TableProps<Item>> {
static defaultProps: TableProps<any> = {
scrollable: true,
autoSize: true,
rowPadding: "8px",
rowLineHeight: "17px",
rowPadding: 8,
rowLineHeight: 17,
sortSyncWithUrl: true,
customRowHeights: (item, lineHeight, paddings) => lineHeight + paddings,
};
constructor(props: TableProps<Item>) {
@ -92,16 +110,22 @@ export class Table<Item> extends React.Component<TableProps<Item>> {
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<TableHeadProps> = content.find(elem => elem.type === TableHead);
@ -109,7 +133,7 @@ export class Table<Item> extends React.Component<TableProps<Item>> {
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<Item> extends React.Component<TableProps<Item>> {
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<Item> extends React.Component<TableProps<Item>> {
orderByUrlParam.set(orderBy);
}
if (onSort) {
onSort({ sortBy, orderBy });
}
onSort?.({ sortBy, orderBy });
}
@boundMethod
@ -183,18 +203,19 @@ export class Table<Item> extends React.Component<TableProps<Item>> {
}
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<TableRowProps>[] = 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<Item> extends React.Component<TableProps<Item>> {
}
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 (
<VirtualList
@ -220,6 +233,7 @@ export class Table<Item> extends React.Component<TableProps<Item>> {
getRow={getTableRow}
selectedItemId={selectedItemId}
className={className}
fixedHeight={virtualHeight}
/>
);
}
@ -228,15 +242,13 @@ export class Table<Item> extends React.Component<TableProps<Item>> {
}
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 (
<div className={className}>
<div className={classNames}>
{this.renderHead()}
{this.renderRows()}
</div>

View File

@ -44,6 +44,12 @@ interface Props<T extends ItemObject = any> {
getRow?: (uid: string | number) => React.ReactElement<any>;
onScroll?: (props: ListOnScrollProps) => void;
outerRef?: React.Ref<any>
/**
* 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<Props, State> {
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 (
<VariableSizeList
className="list"
width={width}
height={height}
itemSize={this.getItemSize}
itemCount={items.length}
itemData={{
items,
getRow
}}
overscanCount={overscanCount}
ref={this.listRef}
outerRef={outerRef}
onScroll={onScroll}
>
{Row}
</VariableSizeList>
);
}
render() {
const { className, fixedHeight } = this.props;
return (
<div className={cssNames("VirtualList", className)}>
<AutoSizer disableWidth>
{({ height }) => (
<VariableSizeList
className="list"
width={width}
height={height}
itemSize={this.getItemSize}
itemCount={items.length}
itemData={rowData}
overscanCount={overscanCount}
ref={this.listRef}
outerRef={outerRef}
onScroll={onScroll}
>
{Row}
</VariableSizeList>
)}
</AutoSizer>
{
typeof fixedHeight === "number"
? this.renderList(fixedHeight)
: (
<AutoSizer disableWidth>
{({ height }) => this.renderList(height)}
</AutoSizer>
)
}
</div>
);
}

View File

@ -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
});
}

View File

@ -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<DialogProps> {
}
@ -48,7 +49,6 @@ const dialogState = observable.object({
@observer
export class PortForwardDialog extends Component<Props> {
@observable ready = false;
@observable currentPort = 0;
@observable desiredPort = 0;
@ -57,7 +57,7 @@ export class PortForwardDialog extends Component<Props> {
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<Props> {
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<Props> {
portForward.forwardPort = desiredPort;
port = await addPortForward(portForward);
}
if (dialogState.openInBrowser) {
portForward.forwardPort = port;
openPortForward(portForward);
@ -120,7 +117,7 @@ export class PortForwardDialog extends Component<Props> {
renderContents() {
return (
<>
<div className="flex gaps align-center">
<div className="flex column gaps align-left">
<div className="input-container flex align-center">
<div className="current-port" data-testid="current-port">
Local port to forward from:
@ -134,6 +131,13 @@ export class PortForwardDialog extends Component<Props> {
onChange={this.changePort}
/>
</div>
<Checkbox
data-testid="port-forward-open"
theme="light"
label="Open in Browser"
value={dialogState.openInBrowser}
onChange={value => dialogState.openInBrowser = value}
/>
</div>
</>
);
@ -162,7 +166,6 @@ export class PortForwardDialog extends Component<Props> {
contentClass="flex gaps column"
next={this.startPortForward}
nextLabel={this.currentPort === 0 ? "Start" : "Restart"}
disabledNext={!this.ready}
>
{this.renderContents()}
</WizardStep>

View File

@ -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 [

View File

@ -81,6 +81,16 @@ export class PortForwardStore extends ItemStore<PortForwardItem> {
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 {

View File

@ -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);
});
});

View File

@ -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<T>(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<string, any> = {}) {
logger.info(`${logPrefix} saving ${filePath}`);
@ -92,11 +86,6 @@ export function createAppStorage<T>(key: string, defaultValue: T, clusterId?: st
});
}
}
function removeFile() {
logger.debug(`${logPrefix} removing ${filePath}`);
fse.unlink(filePath).catch(Function);
}
}
return new StorageHelper<T>(key, {

View File

@ -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";

View File

@ -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};
}
}
}
return isNaN(converted) ? part : converted;
});
}

274
yarn.lock
View File

@ -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"