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:
commit
40f0dc989d
4
Makefile
4
Makefile
@ -39,10 +39,6 @@ dev: binaries/client build-extensions static/build/LensDev.html
|
||||
lint:
|
||||
yarn lint
|
||||
|
||||
.PHONY: release-version
|
||||
release-version:
|
||||
npm version $(CMD_ARGS) --git-tag-version false
|
||||
|
||||
.PHONY: tag-release
|
||||
tag-release:
|
||||
scripts/tag-release.sh $(CMD_ARGS)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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}'`);
|
||||
|
||||
25
package.json
25
package.json
@ -212,7 +212,7 @@
|
||||
"jsonpath": "^1.1.1",
|
||||
"lodash": "^4.17.15",
|
||||
"mac-ca": "^1.0.6",
|
||||
"marked": "^2.0.3",
|
||||
"marked": "^2.1.3",
|
||||
"md5-file": "^5.0.0",
|
||||
"mobx": "^6.3.0",
|
||||
"mobx-observable-history": "^2.0.1",
|
||||
@ -222,7 +222,7 @@
|
||||
"moment-timezone": "^0.5.33",
|
||||
"monaco-editor": "^0.29.1",
|
||||
"monaco-editor-webpack-plugin": "^5.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-fetch": "^2.6.5",
|
||||
"node-pty": "^0.10.1",
|
||||
"npm": "^6.14.15",
|
||||
"p-limit": "^3.1.0",
|
||||
@ -253,7 +253,6 @@
|
||||
"ws": "^7.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emeraldpay/hashicon-react": "^0.4.0",
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
@ -263,7 +262,7 @@
|
||||
"@testing-library/dom": "^8.9.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^11.2.6",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/byline": "^4.2.33",
|
||||
"@types/chart.js": "^2.9.34",
|
||||
"@types/color": "^3.0.2",
|
||||
@ -286,18 +285,18 @@
|
||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/module-alias": "^2.0.1",
|
||||
"@types/node": "14.17.26",
|
||||
"@types/node": "14.17.27",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"@types/npm": "^2.0.32",
|
||||
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"@types/randomcolor": "^0.5.6",
|
||||
"@types/react": "^17.0.29",
|
||||
"@types/react-beautiful-dnd": "^13.1.1",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-router-dom": "^5.3.1",
|
||||
"@types/react-select": "3.1.2",
|
||||
"@types/react-table": "^7.7.6",
|
||||
"@types/react-table": "^7.7.7",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/readable-stream": "^2.3.11",
|
||||
@ -315,7 +314,7 @@
|
||||
"@types/webdriverio": "^4.13.0",
|
||||
"@types/webpack": "^4.41.31",
|
||||
"@types/webpack-dev-server": "^3.11.6",
|
||||
"@types/webpack-env": "^1.16.2",
|
||||
"@types/webpack-env": "^1.16.3",
|
||||
"@types/webpack-node-externals": "^1.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.29.1",
|
||||
@ -330,13 +329,13 @@
|
||||
"electron": "^13.5.1",
|
||||
"electron-builder": "^22.11.11",
|
||||
"electron-notarize": "^0.3.0",
|
||||
"esbuild": "^0.12.24",
|
||||
"esbuild-loader": "^2.15.1",
|
||||
"esbuild": "^0.13.8",
|
||||
"esbuild-loader": "^2.16.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-react": "^7.26.1",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-plugin-unused-imports": "^1.0.1",
|
||||
"eslint-plugin-unused-imports": "^1.1.5",
|
||||
"file-loader": "^6.2.0",
|
||||
"flex.box": "^3.4.4",
|
||||
"fork-ts-checker-webpack-plugin": "^5.2.1",
|
||||
@ -352,7 +351,7 @@
|
||||
"mini-css-extract-plugin": "^1.6.2",
|
||||
"node-gyp": "7.1.2",
|
||||
"node-loader": "^1.0.3",
|
||||
"nodemon": "^2.0.13",
|
||||
"nodemon": "^2.0.14",
|
||||
"playwright": "^1.15.2",
|
||||
"postcss": "^8.3.6",
|
||||
"postcss-loader": "4.3.0",
|
||||
@ -374,7 +373,7 @@
|
||||
"tailwindcss": "^2.2.17",
|
||||
"ts-jest": "26.5.6",
|
||||
"ts-loader": "^7.0.5",
|
||||
"ts-node": "^10.2.1",
|
||||
"ts-node": "^10.3.0",
|
||||
"type-fest": "^1.0.2",
|
||||
"typed-emitter": "^1.3.1",
|
||||
"typedoc": "0.22.6",
|
||||
|
||||
@ -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`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
44
src/common/utils/array.ts
Normal 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>;
|
||||
}
|
||||
@ -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 };
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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"/>}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@ -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))}
|
||||
/>
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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 })}
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
})}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -24,5 +24,10 @@
|
||||
&.warning {
|
||||
@include table-cell-warning;
|
||||
}
|
||||
|
||||
&.status {
|
||||
@include port-forward-status-colors;
|
||||
flex: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
1
src/renderer/components/icon/push_off.svg
Normal file
1
src/renderer/components/icon/push_off.svg
Normal 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 |
@ -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 |
@ -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}
|
||||
/>
|
||||
|
||||
@ -28,7 +28,9 @@ export interface SidebarStorageState {
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultSidebarWidth = 200;
|
||||
|
||||
export const sidebarStorage = createStorage<SidebarStorageState>("sidebar", {
|
||||
width: 200,
|
||||
width: defaultSidebarWidth,
|
||||
expanded: {},
|
||||
});
|
||||
|
||||
@ -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}
|
||||
|
||||
155
src/renderer/components/table/__tests__/getSorted.test.ts
Normal file
155
src/renderer/components/table/__tests__/getSorted.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
68
src/renderer/components/table/sorting.ts
Normal file
68
src/renderer/components/table/sorting.ts
Normal 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;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 [
|
||||
|
||||
@ -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 {
|
||||
|
||||
36
src/renderer/utils/__tests__/converted-name.test.ts
Normal file
36
src/renderer/utils/__tests__/converted-name.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@ -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, {
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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
274
yarn.lock
@ -330,10 +330,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
|
||||
integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==
|
||||
|
||||
"@cspotcode/source-map-support@0.6.1":
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz#118511f316e2e87ee4294761868e254d3da47960"
|
||||
integrity sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==
|
||||
"@cspotcode/source-map-support@0.7.0":
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5"
|
||||
integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==
|
||||
dependencies:
|
||||
"@cspotcode/source-map-consumer" "0.8.0"
|
||||
|
||||
@ -386,21 +386,6 @@
|
||||
dir-compare "^2.4.0"
|
||||
fs-extra "^9.0.1"
|
||||
|
||||
"@emeraldpay/hashicon-react@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@emeraldpay/hashicon-react/-/hashicon-react-0.4.0.tgz#a05a1c9af23721a81450978fad816908fa3246b7"
|
||||
integrity sha512-n+84V9R4g84B62NneYg5mz9Vf1xEBURZoyC4G1K9Rs9C7949D3SxgLhuXYyFKkNO5F5qzms5Qr3LGE5QNEdqJg==
|
||||
dependencies:
|
||||
"@emeraldpay/hashicon" "^0.4.0"
|
||||
react "^16.8.0"
|
||||
|
||||
"@emeraldpay/hashicon@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@emeraldpay/hashicon/-/hashicon-0.4.0.tgz#0dcd4db6d589b4eb30495b9995ff66fc9e57f833"
|
||||
integrity sha512-2EB6Z8gS/DK36+SslIuoHE8wdU6pBqljMTjIYxLC8hBN8n2yhhE+xdmifPjqfqOvF/7Wb5HmoNaPX6RGuBrasg==
|
||||
dependencies:
|
||||
js-sha3 "^0.8.0"
|
||||
|
||||
"@emotion/cache@^10.0.27", "@emotion/cache@^10.0.9":
|
||||
version "10.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
|
||||
@ -1262,10 +1247,10 @@
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@testing-library/dom" "^7.28.1"
|
||||
|
||||
"@testing-library/user-event@^13.2.1":
|
||||
version "13.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.2.1.tgz#7a71a39e50b4a733afbe2916fa2b99966e941f98"
|
||||
integrity sha512-cczlgVl+krjOb3j1625usarNEibI0IFRJrSWX9UsJ1HKYFgCQv9Nb7QAipUDXl3Xdz8NDTsiS78eAkPSxlzTlw==
|
||||
"@testing-library/user-event@^13.5.0":
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295"
|
||||
integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
|
||||
@ -1728,10 +1713,10 @@
|
||||
"@types/node" "*"
|
||||
form-data "^3.0.0"
|
||||
|
||||
"@types/node@*", "@types/node@14.17.26", "@types/node@^14.6.2":
|
||||
version "14.17.26"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.26.tgz#47a53c7e7804490155a4646d60c8e194816d073c"
|
||||
integrity sha512-eSTNkK/nfmnC7IKpOJZixDgG0W2/eHz1qyFN7o/rwwwIHsVRp+G9nbh4BrQ77kbQ2zPu286AQRxkuRLPcR3gXw==
|
||||
"@types/node@*", "@types/node@14.17.27", "@types/node@^14.6.2":
|
||||
version "14.17.27"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.27.tgz#5054610d37bb5f6e21342d0e6d24c494231f3b85"
|
||||
integrity sha512-94+Ahf9IcaDuJTle/2b+wzvjmutxXAEXU6O81JHblYXUg2BDG+dnBy7VxIPHKAyEEDHzCMQydTJuWvrE+Aanzw==
|
||||
|
||||
"@types/node@^10.12.0":
|
||||
version "10.17.24"
|
||||
@ -1820,10 +1805,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
|
||||
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
|
||||
|
||||
"@types/react-beautiful-dnd@^13.1.1":
|
||||
version "13.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#fb3fe24a334cc757d290e75722e4d3c8368ce3a3"
|
||||
integrity sha512-1lBBxVSutE8CQM37Jq7KvJwuA94qaEEqsx+G0dnwzG6Sfwf6JGcNeFk5jjjhJli1q2naeMZm+D/dvT/zyX4QPw==
|
||||
"@types/react-beautiful-dnd@^13.1.2":
|
||||
version "13.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz#510405abb09f493afdfd898bf83995dc6385c130"
|
||||
integrity sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
@ -1870,10 +1855,10 @@
|
||||
"@types/react-dom" "*"
|
||||
"@types/react-transition-group" "*"
|
||||
|
||||
"@types/react-table@^7.7.6":
|
||||
version "7.7.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.6.tgz#4899ccc46b4a1de08cc1daf120842e37e112e51e"
|
||||
integrity sha512-ZMFHh1sG5AGDmhVRpz9mgGByGmBFAqnZ7QnyqGa5iAlKtcSC3vb/gul47lM0kJ1uvlawc+qN5k+++pe+GBdJ+g==
|
||||
"@types/react-table@^7.7.7":
|
||||
version "7.7.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.7.tgz#503be1ce2e06857c11b281629f539358d32671f9"
|
||||
integrity sha512-3l2TP4detx9n5Jt44XhdH7Ku6PYwz6kB83P8W+YcBMUkIHtiw2gsCCcq9c4wyCIcdSwcTlWZ9WqH4PF7Yfbprg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
@ -2106,10 +2091,10 @@
|
||||
"@types/webpack" "^4"
|
||||
http-proxy-middleware "^1.0.0"
|
||||
|
||||
"@types/webpack-env@^1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.2.tgz#8db514b059c1b2ae14ce9d7bb325296de6a9a0fa"
|
||||
integrity sha512-vKx7WNQNZDyJveYcHAm9ZxhqSGLYwoyLhrHjLBOkw3a7cT76sTdjgtwyijhk1MaHyRIuSztcVwrUOO/NEu68Dw==
|
||||
"@types/webpack-env@^1.16.3":
|
||||
version "1.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a"
|
||||
integrity sha512-9gtOPPkfyNoEqCQgx4qJKkuNm/x0R2hKR7fdl7zvTJyHnIisuE/LfvXOsYWL0o3qq6uiBnKZNNNzi3l0y/X+xw==
|
||||
|
||||
"@types/webpack-node-externals@^1.7.1":
|
||||
version "1.7.1"
|
||||
@ -2179,7 +2164,7 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^4.33.0", "@typescript-eslint/eslint-plugin@^4.5.0":
|
||||
"@typescript-eslint/eslint-plugin@^4.33.0":
|
||||
version "4.33.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276"
|
||||
integrity sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==
|
||||
@ -5465,12 +5450,67 @@ es6-promisify@^5.0.0:
|
||||
dependencies:
|
||||
es6-promise "^4.0.3"
|
||||
|
||||
esbuild-loader@^2.15.1:
|
||||
version "2.15.1"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-loader/-/esbuild-loader-2.15.1.tgz#5a3940f5d20317f1a35720efa33e933f97c923e9"
|
||||
integrity sha512-JRBL6uTeWplMbylNBt9gxLKMjD8wKnqGq786QV/cm/nPBSNA9/kC7/vNwCXTDPfYqHoWsjyfH7ub9ekN0kdAYQ==
|
||||
esbuild-android-arm64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.8.tgz#c20e875c3c98164b1ffba9b28637bdf96f5e9e7c"
|
||||
integrity sha512-AilbChndywpk7CdKkNSZ9klxl+9MboLctXd9LwLo3b0dawmOF/i/t2U5d8LM6SbT1Xw36F8yngSUPrd8yPs2RA==
|
||||
|
||||
esbuild-darwin-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.8.tgz#f46e6b471ddbf62265234808a6a1aa91df18a417"
|
||||
integrity sha512-b6sdiT84zV5LVaoF+UoMVGJzR/iE2vNUfUDfFQGrm4LBwM/PWXweKpuu6RD9mcyCq18cLxkP6w/LD/w9DtX3ng==
|
||||
|
||||
esbuild-darwin-arm64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.8.tgz#a991157a6013facd4f2e14159b7da52626c90154"
|
||||
integrity sha512-R8YuPiiJayuJJRUBG4H0VwkEKo6AvhJs2m7Tl0JaIer3u1FHHXwGhMxjJDmK+kXwTFPriSysPvcobXC/UrrZCQ==
|
||||
|
||||
esbuild-freebsd-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.8.tgz#301601d2e443ad458960e359b402a17d9500be9d"
|
||||
integrity sha512-zBn6urrn8FnKC+YSgDxdof9jhPCeU8kR/qaamlV4gI8R3KUaUK162WYM7UyFVAlj9N0MyD3AtB+hltzu4cysTw==
|
||||
|
||||
esbuild-freebsd-arm64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.8.tgz#039a63acc12ec0892006c147ea221e55f9125a9f"
|
||||
integrity sha512-pWW2slN7lGlkx0MOEBoUGwRX5UgSCLq3dy2c8RIOpiHtA87xAUpDBvZK10MykbT+aMfXc0NI2lu1X+6kI34xng==
|
||||
|
||||
esbuild-linux-32@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.8.tgz#c537b67d7e694b60bfa2786581412838c6ba0284"
|
||||
integrity sha512-T0I0ueeKVO/Is0CAeSEOG9s2jeNNb8jrrMwG9QBIm3UU18MRB60ERgkS2uV3fZ1vP2F8i3Z2e3Zju4lg9dhVmw==
|
||||
|
||||
esbuild-linux-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.8.tgz#0092fc8a064001a777bfa0e3b425bb8be8f96e6a"
|
||||
integrity sha512-Bm8SYmFtvfDCIu9sjKppFXzRXn2BVpuCinU1ChTuMtdKI/7aPpXIrkqBNOgPTOQO9AylJJc1Zw6EvtKORhn64w==
|
||||
|
||||
esbuild-linux-arm64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.8.tgz#5cd3f2bb924212971482e8dbc25c4afd09b28110"
|
||||
integrity sha512-X4pWZ+SL+FJ09chWFgRNO3F+YtvAQRcWh0uxKqZSWKiWodAB20flsW/OWFYLXBKiVCTeoGMvENZS/GeVac7+tQ==
|
||||
|
||||
esbuild-linux-arm@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.8.tgz#ad634f96bf2975536907aeb9fdb75a3194f4ddce"
|
||||
integrity sha512-4/HfcC40LJ4GPyboHA+db0jpFarTB628D1ifU+/5bunIgY+t6mHkJWyxWxAAE8wl/ZIuRYB9RJFdYpu1AXGPdg==
|
||||
|
||||
esbuild-linux-mips64le@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.8.tgz#57857edfebf9bf65766dc8be1637f2179c990572"
|
||||
integrity sha512-o7e0D+sqHKT31v+mwFircJFjwSKVd2nbkHEn4l9xQ1hLR+Bv8rnt3HqlblY3+sBdlrOTGSwz0ReROlKUMJyldA==
|
||||
|
||||
esbuild-linux-ppc64le@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.8.tgz#fdb82a059a5b86bb10fb42091b4ebcf488b9cd46"
|
||||
integrity sha512-eZSQ0ERsWkukJp2px/UWJHVNuy0lMoz/HZcRWAbB6reoaBw7S9vMzYNUnflfL3XA6WDs+dZn3ekHE4Y2uWLGig==
|
||||
|
||||
esbuild-loader@^2.16.0:
|
||||
version "2.16.0"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-loader/-/esbuild-loader-2.16.0.tgz#a44a57a77ed2810d6b278579271f77d739aa7bc9"
|
||||
integrity sha512-LCJEwkf+nMJbNmVYNgg/0PaIZDdr5OcHw1qbWAZLkrmBRX+KwHY/yAS6ia98UBtwzk/WhsftUBNB6tfPHgFIxw==
|
||||
dependencies:
|
||||
esbuild "^0.12.21"
|
||||
esbuild "^0.13.4"
|
||||
joycon "^3.0.1"
|
||||
json5 "^2.2.0"
|
||||
loader-utils "^2.0.0"
|
||||
@ -5478,10 +5518,58 @@ esbuild-loader@^2.15.1:
|
||||
type-fest "^1.4.0"
|
||||
webpack-sources "^2.2.0"
|
||||
|
||||
esbuild@^0.12.21, esbuild@^0.12.24:
|
||||
version "0.12.29"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.29.tgz#be602db7c4dc78944a9dbde0d1ea19d36c1f882d"
|
||||
integrity sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==
|
||||
esbuild-netbsd-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.8.tgz#d7879e7123d3b2c04754ece8bd061aa6866deeff"
|
||||
integrity sha512-gZX4kP7gVvOrvX0ZwgHmbuHczQUwqYppxqtoyC7VNd80t5nBHOFXVhWo2Ad/Lms0E8b+wwgI/WjZFTCpUHOg9Q==
|
||||
|
||||
esbuild-openbsd-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.8.tgz#88b280b6cb0a3f6adb60abf27fc506c506a35cf0"
|
||||
integrity sha512-afzza308X4WmcebexbTzAgfEWt9MUkdTvwIa8xOu4CM2qGbl2LanqEl8/LUs8jh6Gqw6WsicEK52GPrS9wvkcw==
|
||||
|
||||
esbuild-sunos-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.8.tgz#229ae7c7703196a58acd0f0291ad9bebda815d63"
|
||||
integrity sha512-mWPZibmBbuMKD+LDN23LGcOZ2EawMYBONMXXHmbuxeT0XxCNwadbCVwUQ/2p5Dp5Kvf6mhrlIffcnWOiCBpiVw==
|
||||
|
||||
esbuild-windows-32@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.8.tgz#892d093e32a21c0c9135e5a0ffdc380aeb70e763"
|
||||
integrity sha512-QsZ1HnWIcnIEApETZWw8HlOhDSWqdZX2SylU7IzGxOYyVcX7QI06ety/aDcn437mwyO7Ph4RrbhB+2ntM8kX8A==
|
||||
|
||||
esbuild-windows-64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.8.tgz#7defd8d79ae3bb7e6f53b65a7190be7daf901686"
|
||||
integrity sha512-76Fb57B9eE/JmJi1QmUW0tRLQZfGo0it+JeYoCDTSlbTn7LV44ecOHIMJSSgZADUtRMWT9z0Kz186bnaB3amSg==
|
||||
|
||||
esbuild-windows-arm64@0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.8.tgz#e59ae004496fd8a5ab67bfc7945a2e47480d6fb9"
|
||||
integrity sha512-HW6Mtq5eTudllxY2YgT62MrVcn7oq2o8TAoAvDUhyiEmRmDY8tPwAhb1vxw5/cdkbukM3KdMYtksnUhF/ekWeg==
|
||||
|
||||
esbuild@^0.13.4, esbuild@^0.13.8:
|
||||
version "0.13.8"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.8.tgz#bd7cc51b881ab067789f88e17baca74724c1ec4f"
|
||||
integrity sha512-A4af7G7YZLfG5OnARJRMtlpEsCkq/zHZQXewgPA864l9D6VjjbH1SuFYK/OSV6BtHwDGkdwyRrX0qQFLnMfUcw==
|
||||
optionalDependencies:
|
||||
esbuild-android-arm64 "0.13.8"
|
||||
esbuild-darwin-64 "0.13.8"
|
||||
esbuild-darwin-arm64 "0.13.8"
|
||||
esbuild-freebsd-64 "0.13.8"
|
||||
esbuild-freebsd-arm64 "0.13.8"
|
||||
esbuild-linux-32 "0.13.8"
|
||||
esbuild-linux-64 "0.13.8"
|
||||
esbuild-linux-arm "0.13.8"
|
||||
esbuild-linux-arm64 "0.13.8"
|
||||
esbuild-linux-mips64le "0.13.8"
|
||||
esbuild-linux-ppc64le "0.13.8"
|
||||
esbuild-netbsd-64 "0.13.8"
|
||||
esbuild-openbsd-64 "0.13.8"
|
||||
esbuild-sunos-64 "0.13.8"
|
||||
esbuild-windows-32 "0.13.8"
|
||||
esbuild-windows-64 "0.13.8"
|
||||
esbuild-windows-arm64 "0.13.8"
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
@ -5567,16 +5655,12 @@ eslint-plugin-react@^7.26.1:
|
||||
semver "^6.3.0"
|
||||
string.prototype.matchall "^4.0.5"
|
||||
|
||||
eslint-plugin-unused-imports@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-1.0.1.tgz#f95f48637bb166ed924ecb5ebf27761a57da4178"
|
||||
integrity sha512-AVDcgeoZZoBH/g5743nvWQK7/V7w2RMILHvogfBnYa1s47los7G2ysEweRx0yJ8pSVnITJvxTBkefQbJowTi3w==
|
||||
eslint-plugin-unused-imports@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-1.1.5.tgz#a2b992ef0faf6c6c75c3815cc47bde76739513c2"
|
||||
integrity sha512-TeV8l8zkLQrq9LBeYFCQmYVIXMjfHgdRQLw7dEZp4ZB3PeR10Y5Uif11heCsHRmhdRIYMoewr1d9ouUHLbLHew==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "^4.5.0"
|
||||
eslint "^7.11.0"
|
||||
eslint-rule-composer "^0.3.0"
|
||||
requireindex "~1.2.0"
|
||||
typescript "^4.0.3"
|
||||
|
||||
eslint-rule-composer@^0.3.0:
|
||||
version "0.3.0"
|
||||
@ -5623,7 +5707,7 @@ eslint-visitor-keys@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8"
|
||||
integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
|
||||
|
||||
eslint@^7.11.0, eslint@^7.32.0:
|
||||
eslint@^7.32.0:
|
||||
version "7.32.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
|
||||
integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
|
||||
@ -8432,11 +8516,6 @@ jpeg-js@^0.4.2:
|
||||
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b"
|
||||
integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==
|
||||
|
||||
js-sha3@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
|
||||
integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
@ -9317,10 +9396,10 @@ map-visit@^1.0.0:
|
||||
dependencies:
|
||||
object-visit "^1.0.0"
|
||||
|
||||
marked@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.3.tgz#3551c4958c4da36897bda2a16812ef1399c8d6b0"
|
||||
integrity sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA==
|
||||
marked@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753"
|
||||
integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==
|
||||
|
||||
marked@^3.0.4:
|
||||
version "3.0.6"
|
||||
@ -9849,11 +9928,18 @@ node-fetch-npm@^2.0.2:
|
||||
json-parse-better-errors "^1.0.0"
|
||||
safe-buffer "^5.1.1"
|
||||
|
||||
node-fetch@2.6.1, node-fetch@^2.6.1:
|
||||
node-fetch@2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
|
||||
node-fetch@^2.6.5:
|
||||
version "2.6.5"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
|
||||
integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-forge@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
|
||||
@ -9958,10 +10044,10 @@ node-pty@^0.10.1:
|
||||
dependencies:
|
||||
nan "^2.14.0"
|
||||
|
||||
nodemon@^2.0.13:
|
||||
version "2.0.13"
|
||||
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.13.tgz#67d40d3a4d5bd840aa785c56587269cfcf5d24aa"
|
||||
integrity sha512-UMXMpsZsv1UXUttCn6gv8eQPhn6DR4BW+txnL3IN5IHqrCwcrT/yWHfL35UsClGXknTH79r5xbu+6J1zNHuSyA==
|
||||
nodemon@^2.0.14:
|
||||
version "2.0.14"
|
||||
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.14.tgz#287c7a2f6cd8a18b07e94cd776ecb6a82e4ba439"
|
||||
integrity sha512-frcpDx+PviKEQRSYzwhckuO2zoHcBYLHI754RE9z5h1RGtrngerc04mLpQQCPWBkH/2ObrX7We9YiwVSYZpFJQ==
|
||||
dependencies:
|
||||
chokidar "^3.2.2"
|
||||
debug "^3.2.6"
|
||||
@ -11677,15 +11763,6 @@ react-window@^1.8.5:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
memoize-one ">=3.1.1 <6"
|
||||
|
||||
react@^16.8.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
|
||||
integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||
@ -12042,11 +12119,6 @@ require-main-filename@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
||||
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
||||
|
||||
requireindex@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
|
||||
integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==
|
||||
|
||||
requires-port@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
@ -13649,6 +13721,11 @@ tr46@^2.1.0:
|
||||
dependencies:
|
||||
punycode "^2.1.1"
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
|
||||
|
||||
tree-kill@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
||||
@ -13698,12 +13775,12 @@ ts-loader@^7.0.5:
|
||||
micromatch "^4.0.0"
|
||||
semver "^6.0.0"
|
||||
|
||||
ts-node@^10.2.1:
|
||||
version "10.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.2.1.tgz#4cc93bea0a7aba2179497e65bb08ddfc198b3ab5"
|
||||
integrity sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==
|
||||
ts-node@^10.3.0:
|
||||
version "10.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.3.0.tgz#a797f2ed3ff50c9a5d814ce400437cb0c1c048b4"
|
||||
integrity sha512-RYIy3i8IgpFH45AX4fQHExrT8BxDeKTdC83QFJkNzkvt8uFB6QJ8XMyhynYiKMLxt9a7yuXaDBZNOYS3XjDcYw==
|
||||
dependencies:
|
||||
"@cspotcode/source-map-support" "0.6.1"
|
||||
"@cspotcode/source-map-support" "0.7.0"
|
||||
"@tsconfig/node10" "^1.0.7"
|
||||
"@tsconfig/node12" "^1.0.7"
|
||||
"@tsconfig/node14" "^1.0.0"
|
||||
@ -13886,7 +13963,7 @@ typescript-plugin-css-modules@^3.4.0:
|
||||
stylus "^0.54.8"
|
||||
tsconfig-paths "^3.9.0"
|
||||
|
||||
typescript@^4.0.3, typescript@^4.4.3:
|
||||
typescript@^4.4.3:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324"
|
||||
integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==
|
||||
@ -14290,6 +14367,11 @@ wcwidth@^1.0.0:
|
||||
dependencies:
|
||||
defaults "^1.0.3"
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
|
||||
|
||||
webidl-conversions@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
|
||||
@ -14456,6 +14538,14 @@ whatwg-mimetype@^2.3.0:
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
|
||||
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
whatwg-url@^8.0.0, whatwg-url@^8.5.0:
|
||||
version "8.7.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user