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

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

This commit is contained in:
Roman 2020-10-30 10:29:31 +02:00
commit 9cda855038
20 changed files with 576 additions and 98 deletions

View File

@ -20,6 +20,12 @@
"integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==", "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==",
"dev": true "dev": true
}, },
"@types/json-schema": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
"integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
"dev": true
},
"@types/node": { "@types/node": {
"version": "14.11.11", "version": "14.11.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.11.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.11.tgz",
@ -741,6 +747,12 @@
"unset-value": "^1.0.0" "unset-value": "^1.0.0"
} }
}, },
"camelcase": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz",
"integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==",
"dev": true
},
"chalk": { "chalk": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@ -842,6 +854,12 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true "dev": true
}, },
"colorette": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==",
"dev": true
},
"commander": { "commander": {
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@ -980,6 +998,71 @@
"randomfill": "^1.0.3" "randomfill": "^1.0.3"
} }
}, },
"css-loader": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.0.0.tgz",
"integrity": "sha512-9g35eXRBgjvswyJWoqq/seWp+BOxvUl8IinVNTsUBFFxtwfEYvlmEn6ciyn0liXGbGh5HyJjPGCuobDSfqMIVg==",
"dev": true,
"requires": {
"camelcase": "^6.1.0",
"cssesc": "^3.0.0",
"icss-utils": "^5.0.0",
"loader-utils": "^2.0.0",
"postcss": "^8.1.1",
"postcss-modules-extract-imports": "^3.0.0",
"postcss-modules-local-by-default": "^4.0.0",
"postcss-modules-scope": "^3.0.0",
"postcss-modules-values": "^4.0.0",
"postcss-value-parser": "^4.1.0",
"schema-utils": "^3.0.0",
"semver": "^7.3.2"
},
"dependencies": {
"json5": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"schema-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.6",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
}
},
"semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true
}
}
},
"cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true
},
"csstype": { "csstype": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz",
@ -1600,6 +1683,12 @@
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
"dev": true "dev": true
}, },
"icss-utils": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.0.0.tgz",
"integrity": "sha512-aF2Cf/CkEZrI/vsu5WI/I+akFgdbwQHVE9YRZxATrhH4PVIe6a3BIjwjEcW+z+jP/hNh+YvM3lAAn1wJQ6opSg==",
"dev": true
},
"ieee754": { "ieee754": {
"version": "1.1.13", "version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
@ -1618,6 +1707,12 @@
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
"dev": true "dev": true
}, },
"indexes-of": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
"integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
"dev": true
},
"infer-owner": { "infer-owner": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
@ -1810,6 +1905,33 @@
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true "dev": true
}, },
"klona": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz",
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==",
"dev": true
},
"line-column": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz",
"integrity": "sha1-0lryk2tvSEkXKzEuR5LR2Ye8NKI=",
"dev": true,
"requires": {
"isarray": "^1.0.0",
"isobject": "^2.0.0"
},
"dependencies": {
"isobject": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
"integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
"dev": true,
"requires": {
"isarray": "1.0.0"
}
}
}
},
"loader-runner": { "loader-runner": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
@ -2051,6 +2173,12 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"nanoid": {
"version": "3.1.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.16.tgz",
"integrity": "sha512-+AK8MN0WHji40lj8AEuwLOvLSbWYApQpre/aFJZD71r43wVRLrOYS4FmJOPQYon1TqB462RzrrxlfA74XRES8w==",
"dev": true
},
"nanomatch": { "nanomatch": {
"version": "1.2.13", "version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -2317,6 +2445,71 @@
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
"dev": true "dev": true
}, },
"postcss": {
"version": "8.1.4",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.1.4.tgz",
"integrity": "sha512-LfqcwgMq9LOd8pX7K2+r2HPitlIGC5p6PoZhVELlqhh2YGDVcXKpkCseqan73Hrdik6nBd2OvoDPUaP/oMj9hQ==",
"dev": true,
"requires": {
"colorette": "^1.2.1",
"line-column": "^1.0.2",
"nanoid": "^3.1.15",
"source-map": "^0.6.1"
}
},
"postcss-modules-extract-imports": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
"integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
"dev": true
},
"postcss-modules-local-by-default": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz",
"integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==",
"dev": true,
"requires": {
"icss-utils": "^5.0.0",
"postcss-selector-parser": "^6.0.2",
"postcss-value-parser": "^4.1.0"
}
},
"postcss-modules-scope": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz",
"integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==",
"dev": true,
"requires": {
"postcss-selector-parser": "^6.0.4"
}
},
"postcss-modules-values": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
"integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
"dev": true,
"requires": {
"icss-utils": "^5.0.0"
}
},
"postcss-selector-parser": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz",
"integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==",
"dev": true,
"requires": {
"cssesc": "^3.0.0",
"indexes-of": "^1.0.1",
"uniq": "^1.0.1",
"util-deprecate": "^1.0.2"
}
},
"postcss-value-parser": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
"integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==",
"dev": true
},
"process": { "process": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@ -2576,6 +2769,58 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true "dev": true
}, },
"sass-loader": {
"version": "10.0.4",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.4.tgz",
"integrity": "sha512-zhdZ8qvZM4iL5XjLVEjJLvKWvC+MB+hHgzL2x/Nf7UHpUNmPYsJvypW79bW39g4LZ603dH/dRSsRYzJJIljtdA==",
"dev": true,
"requires": {
"klona": "^2.0.4",
"loader-utils": "^2.0.0",
"neo-async": "^2.6.2",
"schema-utils": "^3.0.0",
"semver": "^7.3.2"
},
"dependencies": {
"json5": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"schema-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.6",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
}
},
"semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true
}
}
},
"schema-utils": { "schema-utils": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
@ -2882,6 +3127,49 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"style-loader": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz",
"integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==",
"dev": true,
"requires": {
"loader-utils": "^2.0.0",
"schema-utils": "^3.0.0"
},
"dependencies": {
"json5": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"schema-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.6",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
}
}
}
},
"supports-color": { "supports-color": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@ -3053,6 +3341,12 @@
"set-value": "^2.0.1" "set-value": "^2.0.1"
} }
}, },
"uniq": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
"integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
"dev": true
},
"unique-filename": { "unique-filename": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",

View File

@ -10,13 +10,16 @@
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"@types/node": "^14.11.11", "@types/node": "^14.11.11",
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
"@types/react-router": "^5.1.8", "@types/react-router": "^5.1.8",
"@types/webpack": "^4.41.17", "@types/webpack": "^4.41.17",
"@k8slens/extensions": "file:../../src/extensions/npm/extensions", "css-loader": "^5.0.0",
"mobx": "^5.15.5", "mobx": "^5.15.5",
"react": "^16.13.1", "react": "^16.13.1",
"sass-loader": "^10.0.4",
"style-loader": "^2.0.0",
"ts-loader": "^8.0.4", "ts-loader": "^8.0.4",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"typescript": "^4.0.3", "typescript": "^4.0.3",

View File

@ -0,0 +1,13 @@
.PageLayout.Support {
a[target=_blank] {
text-decoration: none;
border-bottom: 1px solid;
&:after {
content: "launch";
font: small "Material Icons";
vertical-align: middle;
margin-left: 2px;
}
}
}

View File

@ -1,6 +1,6 @@
// TODO: figure out how to consume styles / handle import "./support.scss"
// TODO: support localization / figure out how to extract / consume i18n strings // TODO: support localization / figure out how to extract / consume i18n strings
import "./support.scss"
import React from "react" import React from "react"
import { observer } from "mobx-react" import { observer } from "mobx-react"
import { App, Component } from "@k8slens/extensions"; import { App, Component } from "@k8slens/extensions";

View File

@ -2,7 +2,6 @@ import path from "path"
const outputPath = path.resolve(__dirname, 'dist'); const outputPath = path.resolve(__dirname, 'dist');
// TODO: figure out how to share base TS and Webpack configs from Lens (npm, filesystem, etc?)
const lensExternals = { const lensExternals = {
"@k8slens/extensions": "var global.LensExtensions", "@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React", "react": "var global.React",
@ -50,6 +49,14 @@ export default [
use: 'ts-loader', use: 'ts-loader',
exclude: /node_modules/, exclude: /node_modules/,
}, },
{
test: /\.s?css$/,
use: [
"style-loader",
"css-loader",
"sass-loader",
]
}
], ],
}, },
externals: [ externals: [

View File

@ -64,13 +64,13 @@ describe("empty config", () => {
it("sets active cluster", () => { it("sets active cluster", () => {
clusterStore.setActive("foo"); clusterStore.setActive("foo");
expect(clusterStore.activeCluster.id).toBe("foo"); expect(clusterStore.active.id).toBe("foo");
}) })
}) })
describe("with prod and dev clusters added", () => { describe("with prod and dev clusters added", () => {
beforeEach(() => { beforeEach(() => {
clusterStore.addCluster( clusterStore.addClusters(
new Cluster({ new Cluster({
id: "prod", id: "prod",
contextName: "prod", contextName: "prod",

View File

@ -10,7 +10,7 @@ jest.mock("electron", () => {
} }
}) })
import { WorkspaceStore } from "../workspace-store" import { Workspace, WorkspaceStore } from "../workspace-store"
describe("workspace store tests", () => { describe("workspace store tests", () => {
describe("for an empty config", () => { describe("for an empty config", () => {
@ -35,16 +35,16 @@ describe("workspace store tests", () => {
it("cannot remove the default workspace", () => { it("cannot remove the default workspace", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
expect(() => ws.removeWorkspace(WorkspaceStore.defaultId)).toThrowError("Cannot remove"); expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove");
}) })
it("can update default workspace name", () => { it("can update default workspace name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: WorkspaceStore.defaultId, id: WorkspaceStore.defaultId,
name: "foobar", name: "foobar",
}); }));
expect(ws.currentWorkspace.name).toBe("foobar"); expect(ws.currentWorkspace.name).toBe("foobar");
}) })
@ -52,10 +52,10 @@ describe("workspace store tests", () => {
it("can add workspaces", () => { it("can add workspaces", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "123", id: "123",
name: "foobar", name: "foobar",
}); }));
expect(ws.getById("123").name).toBe("foobar"); expect(ws.getById("123").name).toBe("foobar");
}) })
@ -69,10 +69,10 @@ describe("workspace store tests", () => {
it("can set a existent workspace to be active", () => { it("can set a existent workspace to be active", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "abc", id: "abc",
name: "foobar", name: "foobar",
}); }));
expect(() => ws.setActive("abc")).not.toThrowError(); expect(() => ws.setActive("abc")).not.toThrowError();
}) })
@ -80,15 +80,15 @@ describe("workspace store tests", () => {
it("can remove a workspace", () => { it("can remove a workspace", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "123", id: "123",
name: "foobar", name: "foobar",
}); }));
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "1234", id: "1234",
name: "foobar 1", name: "foobar 1",
}); }));
ws.removeWorkspace("123"); ws.removeWorkspaceById("123");
expect(ws.workspaces.size).toBe(2); expect(ws.workspaces.size).toBe(2);
}) })
@ -96,10 +96,10 @@ describe("workspace store tests", () => {
it("cannot create workspace with existent name", () => { it("cannot create workspace with existent name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "someid", id: "someid",
name: "default", name: "default",
}); }));
expect(ws.workspacesList.length).toBe(1); // default workspace only expect(ws.workspacesList.length).toBe(1); // default workspace only
}) })
@ -107,10 +107,10 @@ describe("workspace store tests", () => {
it("cannot create workspace with empty name", () => { it("cannot create workspace with empty name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "random", id: "random",
name: "", name: "",
}); }));
expect(ws.workspacesList.length).toBe(1); // default workspace only expect(ws.workspacesList.length).toBe(1); // default workspace only
}) })
@ -118,10 +118,10 @@ describe("workspace store tests", () => {
it("cannot create workspace with ' ' name", () => { it("cannot create workspace with ' ' name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "random", id: "random",
name: " ", name: " ",
}); }));
expect(ws.workspacesList.length).toBe(1); // default workspace only expect(ws.workspacesList.length).toBe(1); // default workspace only
}) })
@ -129,10 +129,10 @@ describe("workspace store tests", () => {
it("trim workspace name", () => { it("trim workspace name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "random", id: "random",
name: "default ", name: "default ",
}); }));
expect(ws.workspacesList.length).toBe(1); // default workspace only expect(ws.workspacesList.length).toBe(1); // default workspace only
}) })

View File

@ -33,11 +33,12 @@ export type ClusterId = string;
export interface ClusterModel { export interface ClusterModel {
id: ClusterId; id: ClusterId;
kubeConfigPath: string;
workspace?: WorkspaceId; workspace?: WorkspaceId;
contextName?: string; contextName?: string;
preferences?: ClusterPreferences; preferences?: ClusterPreferences;
metadata?: ClusterMetadata; metadata?: ClusterMetadata;
kubeConfigPath: string; ownerRef?: string;
/** @deprecated */ /** @deprecated */
kubeConfig?: string; // yaml kubeConfig?: string; // yaml
@ -72,25 +73,34 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return filePath; return filePath;
} }
@observable activeCluster: ClusterId;
@observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>();
private constructor() { private constructor() {
super({ super({
configName: "lens-cluster-store", configName: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
migrations: migrations, migrations: migrations,
}); });
this.pushStateToViewsPeriodically()
} }
@observable activeClusterId: ClusterId; protected pushStateToViewsPeriodically() {
@observable removedClusters = observable.map<ClusterId, Cluster>(); if (!ipcRenderer) {
@observable clusters = observable.map<ClusterId, Cluster>(); // This is a bit of a hack, we need to do this because we might loose messages that are sent before a view is ready
setInterval(() => {
this.pushState()
}, 5000)
}
}
registerIpcListener() { registerIpcListener() {
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`) logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`)
ipcRenderer.on("cluster:state", (event, model: ClusterState) => { ipcRenderer.on("cluster:state", (event, clusterId: string, state: ClusterState) => {
this.applyWithoutSync(() => { logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state);
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, model); this.getById(clusterId)?.setState(state)
this.getById(model.id)?.updateModel(model);
})
}) })
} }
@ -99,21 +109,35 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
ipcRenderer.removeAllListeners("cluster:state") ipcRenderer.removeAllListeners("cluster:state")
} }
@computed get activeCluster(): Cluster | null { pushState() {
return this.getById(this.activeClusterId); this.clusters.forEach((c) => {
c.pushState()
})
}
get activeClusterId() {
return this.activeCluster
} }
@computed get clustersList(): Cluster[] { @computed get clustersList(): Cluster[] {
return Array.from(this.clusters.values()); return Array.from(this.clusters.values());
} }
@computed get enabledClustersList(): Cluster[] {
return this.clustersList.filter((c) => c.enabled)
}
@computed get active(): Cluster | null {
return this.getById(this.activeCluster);
}
isActive(id: ClusterId) { isActive(id: ClusterId) {
return this.activeClusterId === id; return this.activeCluster === id;
} }
@action @action
setActive(id: ClusterId) { setActive(id: ClusterId) {
this.activeClusterId = this.clusters.has(id) ? id : null; this.activeCluster = this.clusters.has(id) ? id : null;
} }
@action @action
@ -145,12 +169,28 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
@action @action
addCluster(...models: ClusterModel[]) { addClusters(...models: ClusterModel[]): Cluster[] {
const clusters: Cluster[] = []
models.forEach(model => { models.forEach(model => {
appEventBus.emit({name: "cluster", action: "add"}) clusters.push(this.addCluster(model))
const cluster = new Cluster(model);
this.clusters.set(model.id, cluster);
}) })
return clusters
}
@action
addCluster(model: ClusterModel | Cluster ): Cluster {
appEventBus.emit({name: "cluster", action: "add"})
let cluster = model as Cluster;
if (!(model instanceof Cluster)) {
cluster = new Cluster(model)
}
this.clusters.set(model.id, cluster);
return cluster
}
async removeCluster(model: ClusterModel) {
await this.removeById(model.id)
} }
@action @action
@ -159,7 +199,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
const cluster = this.getById(clusterId); const cluster = this.getById(clusterId);
if (cluster) { if (cluster) {
this.clusters.delete(clusterId); this.clusters.delete(clusterId);
if (this.activeClusterId === clusterId) { if (this.activeCluster === clusterId) {
this.setActive(null); this.setActive(null);
} }
// remove only custom kubeconfigs (pasted as text) // remove only custom kubeconfigs (pasted as text)
@ -189,6 +229,9 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
cluster.updateModel(clusterModel); cluster.updateModel(clusterModel);
} else { } else {
cluster = new Cluster(clusterModel); cluster = new Cluster(clusterModel);
if (!cluster.isManaged) {
cluster.enabled = true
}
} }
newClusters.set(clusterModel.id, cluster); newClusters.set(clusterModel.id, cluster);
} }
@ -200,14 +243,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
}); });
this.activeClusterId = newClusters.has(activeCluster) ? activeCluster : null; this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null;
this.clusters.replace(newClusters); this.clusters.replace(newClusters);
this.removedClusters.replace(removedClusters); this.removedClusters.replace(removedClusters);
} }
toJSON(): ClusterStoreModel { toJSON(): ClusterStoreModel {
return toJS({ return toJS({
activeCluster: this.activeClusterId, activeCluster: this.activeCluster,
clusters: this.clustersList.map(cluster => cluster.toJSON()), clusters: this.clustersList.map(cluster => cluster.toJSON()),
}, { }, {
recurseEverything: true recurseEverything: true

View File

@ -1,19 +1,77 @@
import { action, computed, observable, toJS } from "mobx"; import { ipcRenderer } from "electron";
import { action, computed, observable, toJS, reaction } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
import { clusterStore } from "./cluster-store" import { clusterStore } from "./cluster-store"
import { appEventBus } from "./event-bus"; import { appEventBus } from "./event-bus";
import { broadcastIpc } from "../common/ipc";
import logger from "../main/logger";
export type WorkspaceId = string; export type WorkspaceId = string;
export interface WorkspaceStoreModel { export interface WorkspaceStoreModel {
currentWorkspace?: WorkspaceId; currentWorkspace?: WorkspaceId;
workspaces: Workspace[] workspaces: WorkspaceModel[]
} }
export interface Workspace { export interface WorkspaceModel {
id: WorkspaceId; id: WorkspaceId;
name: string; name: string;
description?: string; description?: string;
ownerRef?: string;
}
export interface WorkspaceState {
enabled: boolean;
}
export class Workspace implements WorkspaceModel, WorkspaceState {
@observable id: WorkspaceId
@observable name: string
@observable description?: string
@observable ownerRef?: string
@observable enabled: boolean
constructor(data: WorkspaceModel) {
Object.assign(this, data)
if (!ipcRenderer) {
reaction(() => this.getState(), () => {
this.pushState()
})
}
}
get isManaged(): boolean {
return !!this.ownerRef
}
getState(): WorkspaceState {
return {
enabled: this.enabled
}
}
pushState(state = this.getState()) {
logger.silly("[WORKSPACE] pushing state", {...state, id: this.id})
broadcastIpc({
channel: "workspace:state",
args: [this.id, toJS(state)],
});
}
@action
setState(state: WorkspaceState) {
Object.assign(this, state)
}
toJSON(): WorkspaceModel {
return toJS({
id: this.id,
name: this.name,
description: this.description,
ownerRef: this.ownerRef
})
}
} }
export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> { export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
@ -23,15 +81,33 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
super({ super({
configName: "lens-workspace-store", configName: "lens-workspace-store",
}); });
if (!ipcRenderer) {
setInterval(() => {
this.pushState()
}, 5000)
}
}
registerIpcListener() {
logger.info("[WORKSPACE-STORE] starting to listen state events")
ipcRenderer.on("workspace:state", (event, workspaceId: string, state: WorkspaceState) => {
this.getById(workspaceId)?.setState(state)
})
}
unregisterIpcListener() {
super.unregisterIpcListener()
ipcRenderer.removeAllListeners("workspace:state")
} }
@observable currentWorkspaceId = WorkspaceStore.defaultId; @observable currentWorkspaceId = WorkspaceStore.defaultId;
@observable workspaces = observable.map<WorkspaceId, Workspace>({ @observable workspaces = observable.map<WorkspaceId, Workspace>({
[WorkspaceStore.defaultId]: { [WorkspaceStore.defaultId]: new Workspace({
id: WorkspaceStore.defaultId, id: WorkspaceStore.defaultId,
name: "default" name: "default"
} })
}); });
@computed get currentWorkspace(): Workspace { @computed get currentWorkspace(): Workspace {
@ -42,6 +118,16 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
return Array.from(this.workspaces.values()); return Array.from(this.workspaces.values());
} }
@computed get enabledWorkspacesList() {
return this.workspacesList.filter((w) => w.enabled);
}
pushState() {
this.workspaces.forEach((w) => {
w.pushState()
})
}
isDefault(id: WorkspaceId) { isDefault(id: WorkspaceId) {
return id === WorkspaceStore.defaultId; return id === WorkspaceStore.defaultId;
} }
@ -61,11 +147,11 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
throw new Error(`workspace ${id} doesn't exist`); throw new Error(`workspace ${id} doesn't exist`);
} }
this.currentWorkspaceId = id; this.currentWorkspaceId = id;
clusterStore.activeClusterId = null; // fixme: handle previously selected cluster from current workspace clusterStore.activeCluster = null; // fixme: handle previously selected cluster from current workspace
} }
@action @action
saveWorkspace(workspace: Workspace) { addWorkspace(workspace: Workspace) {
const { id, name } = workspace; const { id, name } = workspace;
const existingWorkspace = this.getById(id); const existingWorkspace = this.getById(id);
if (!name.trim() || this.getByName(name.trim())) { if (!name.trim() || this.getByName(name.trim())) {
@ -82,7 +168,12 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
} }
@action @action
removeWorkspace(id: WorkspaceId) { removeWorkspace(workspace: Workspace) {
this.removeWorkspaceById(workspace.id)
}
@action
removeWorkspaceById(id: WorkspaceId) {
const workspace = this.getById(id); const workspace = this.getById(id);
if (!workspace) return; if (!workspace) return;
if (this.isDefault(id)) { if (this.isDefault(id)) {
@ -103,7 +194,11 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
} }
if (workspaces.length) { if (workspaces.length) {
this.workspaces.clear(); this.workspaces.clear();
workspaces.forEach(workspace => { workspaces.forEach(ws => {
const workspace = new Workspace(ws)
if (!workspace.isManaged) {
workspace.enabled = true
}
this.workspaces.set(workspace.id, workspace) this.workspaces.set(workspace.id, workspace)
}) })
} }
@ -112,7 +207,7 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
toJSON(): WorkspaceStoreModel { toJSON(): WorkspaceStoreModel {
return toJS({ return toJS({
currentWorkspace: this.currentWorkspaceId, currentWorkspace: this.currentWorkspaceId,
workspaces: this.workspacesList, workspaces: this.workspacesList.map((w) => w.toJSON()),
}, { }, {
recurseEverything: true recurseEverything: true
}) })

View File

@ -1,4 +1,4 @@
export { ExtensionStore } from "../extension-store" export { ExtensionStore } from "../extension-store"
export { clusterStore, ClusterModel } from "../../common/cluster-store" export { clusterStore, ClusterModel } from "../../common/cluster-store"
export { workspaceStore} from "../../common/workspace-store" export { Cluster } from "../../main/cluster"
export type { Cluster } from "../../main/cluster" export { workspaceStore, Workspace, WorkspaceModel } from "../../common/workspace-store"

View File

@ -10,7 +10,7 @@ export class ClusterManager {
constructor(public readonly port: number) { constructor(public readonly port: number) {
// auto-init clusters // auto-init clusters
autorun(() => { autorun(() => {
clusterStore.clusters.forEach(cluster => { clusterStore.enabledClustersList.forEach(cluster => {
if (!cluster.initialized) { if (!cluster.initialized) {
logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta()); logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta());
cluster.init(port); cluster.init(port);

View File

@ -1,3 +1,4 @@
import { ipcMain } from "electron"
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
import type { WorkspaceId } from "../common/workspace-store"; import type { WorkspaceId } from "../common/workspace-store";
@ -33,7 +34,7 @@ export type ClusterRefreshOptions = {
refreshMetadata?: boolean refreshMetadata?: boolean
} }
export interface ClusterState extends ClusterModel { export interface ClusterState {
initialized: boolean; initialized: boolean;
apiUrl: string; apiUrl: string;
online: boolean; online: boolean;
@ -47,11 +48,12 @@ export interface ClusterState extends ClusterModel {
allowedResources: string[] allowedResources: string[]
} }
export class Cluster implements ClusterModel { export class Cluster implements ClusterModel, ClusterState {
public id: ClusterId; public id: ClusterId;
public frameId: number; public frameId: number;
public kubeCtl: Kubectl public kubeCtl: Kubectl
public contextHandler: ContextHandler; public contextHandler: ContextHandler;
public ownerRef: string;
protected kubeconfigManager: KubeconfigManager; protected kubeconfigManager: KubeconfigManager;
protected eventDisposers: Function[] = []; protected eventDisposers: Function[] = [];
protected activated = false; protected activated = false;
@ -65,6 +67,7 @@ export class Cluster implements ClusterModel {
@observable kubeConfigPath: string; @observable kubeConfigPath: string;
@observable apiUrl: string; // cluster server url @observable apiUrl: string; // cluster server url
@observable kubeProxyUrl: string; // lens-proxy to kube-api url @observable kubeProxyUrl: string; // lens-proxy to kube-api url
@observable enabled = false;
@observable online = false; @observable online = false;
@observable accessible = false; @observable accessible = false;
@observable ready = false; @observable ready = false;
@ -81,6 +84,7 @@ export class Cluster implements ClusterModel {
@computed get available() { @computed get available() {
return this.accessible && !this.disconnected; return this.accessible && !this.disconnected;
} }
get version(): string { get version(): string {
return String(this.metadata?.version) || "" return String(this.metadata?.version) || ""
} }
@ -93,6 +97,10 @@ export class Cluster implements ClusterModel {
} }
} }
get isManaged(): boolean {
return !!this.ownerRef
}
@action @action
updateModel(model: ClusterModel) { updateModel(model: ClusterModel) {
Object.assign(this, model); Object.assign(this, model);
@ -123,13 +131,15 @@ export class Cluster implements ClusterModel {
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes
this.eventDisposers.push( if (ipcMain) {
reaction(this.getState, this.pushState), this.eventDisposers.push(
() => { reaction(() => this.getState(), () => this.pushState()),
clearInterval(refreshTimer); () => {
clearInterval(refreshMetadataTimer); clearInterval(refreshTimer);
}, clearInterval(refreshMetadataTimer);
); },
);
}
} }
protected unbindEvents() { protected unbindEvents() {
@ -361,6 +371,7 @@ export class Cluster implements ClusterModel {
workspace: this.workspace, workspace: this.workspace,
preferences: this.preferences, preferences: this.preferences,
metadata: this.metadata, metadata: this.metadata,
ownerRef: this.ownerRef
}; };
return toJS(model, { return toJS(model, {
recurseEverything: true recurseEverything: true
@ -368,9 +379,8 @@ export class Cluster implements ClusterModel {
} }
// serializable cluster-state used for sync btw main <-> renderer // serializable cluster-state used for sync btw main <-> renderer
getState = (): ClusterState => { getState(): ClusterState {
const state: ClusterState = { const state: ClusterState = {
...this.toJSON(),
initialized: this.initialized, initialized: this.initialized,
apiUrl: this.apiUrl, apiUrl: this.apiUrl,
online: this.online, online: this.online,
@ -388,14 +398,18 @@ export class Cluster implements ClusterModel {
}) })
} }
pushState = (state = this.getState()): ClusterState => { @action
setState(state: ClusterState) {
Object.assign(this, state)
}
pushState(state = this.getState()) {
logger.silly(`[CLUSTER]: push-state`, state); logger.silly(`[CLUSTER]: push-state`, state);
broadcastIpc({ broadcastIpc({
channel: "cluster:state", channel: "cluster:state",
frameId: this.frameId, frameId: this.frameId,
args: [state], args: [this.id, state],
}); })
return state;
} }
// get cluster system meta, e.g. use in "logger" // get cluster system meta, e.g. use in "logger"

View File

@ -80,7 +80,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
}, },
{ {
label: "Clusters", label: "Clusters",
submenu: workspaceStore.workspacesList submenu: workspaceStore.enabledWorkspacesList
.filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces .filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces
.map(workspace => { .map(workspace => {
const clusters = clusterStore.getByWorkspaceId(workspace.id); const clusters = clusterStore.getByWorkspaceId(workspace.id);

View File

@ -40,6 +40,7 @@ export async function bootstrap(App: AppComponent) {
// Register additional store listeners // Register additional store listeners
clusterStore.registerIpcListener(); clusterStore.registerIpcListener();
workspaceStore.registerIpcListener();
// init app's dependencies if any // init app's dependencies if any
if (App.init) { if (App.init) {

View File

@ -163,7 +163,7 @@ export class AddCluster extends React.Component {
}) })
runInAction(() => { runInAction(() => {
clusterStore.addCluster(...newClusters); clusterStore.addClusters(...newClusters);
if (newClusters.length === 1) { if (newClusters.length === 1) {
const clusterId = newClusters[0].id; const clusterId = newClusters[0].id;
clusterStore.setActive(clusterId); clusterStore.setActive(clusterId);

View File

@ -26,7 +26,7 @@ export class ClusterWorkspaceSetting extends React.Component<Props> {
<Select <Select
value={this.props.cluster.workspace} value={this.props.cluster.workspace}
onChange={({value}) => this.props.cluster.workspace = value} onChange={({value}) => this.props.cluster.workspace = value}
options={workspaceStore.workspacesList.map(w => options={workspaceStore.enabledWorkspacesList.map(w =>
({value: w.id, label: w.name}) ({value: w.id, label: w.name})
)} )}
/> />

View File

@ -28,8 +28,9 @@ export class RemoveClusterButton extends React.Component<Props> {
} }
render() { render() {
const { cluster } = this.props;
return ( return (
<Button accent onClick={this.confirmRemoveCluster} className="button-area"> <Button accent onClick={this.confirmRemoveCluster} className="button-area" disabled={cluster.isManaged}>
Remove Cluster Remove Cluster
</Button> </Button>
); );

View File

@ -19,7 +19,7 @@ export class WorkspaceMenu extends React.Component<Props> {
render() { render() {
const { className, ...menuProps } = this.props; const { className, ...menuProps } = this.props;
const { workspacesList, currentWorkspace } = workspaceStore; const { enabledWorkspacesList, currentWorkspace } = workspaceStore;
return ( return (
<Menu <Menu
{...menuProps} {...menuProps}
@ -32,7 +32,7 @@ export class WorkspaceMenu extends React.Component<Props> {
<Link className="workspaces-title" to={workspacesURL()}> <Link className="workspaces-title" to={workspacesURL()}>
<Trans>Workspaces</Trans> <Trans>Workspaces</Trans>
</Link> </Link>
{workspacesList.map(({ id: workspaceId, name, description }) => { {enabledWorkspacesList.map(({ id: workspaceId, name, description }) => {
return ( return (
<MenuItem <MenuItem
key={workspaceId} key={workspaceId}

View File

@ -19,8 +19,12 @@ export class Workspaces extends React.Component {
@observable editingWorkspaces = observable.map<WorkspaceId, Workspace>(); @observable editingWorkspaces = observable.map<WorkspaceId, Workspace>();
@computed get workspaces(): Workspace[] { @computed get workspaces(): Workspace[] {
const currentWorkspaces: Map<WorkspaceId, Workspace> = new Map()
workspaceStore.enabledWorkspacesList.forEach((w) => {
currentWorkspaces.set(w.id, w)
})
const allWorkspaces = new Map([ const allWorkspaces = new Map([
...workspaceStore.workspaces, ...currentWorkspaces,
...this.editingWorkspaces, ...this.editingWorkspaces,
]); ]);
return Array.from(allWorkspaces.values()); return Array.from(allWorkspaces.values());
@ -42,7 +46,7 @@ export class Workspaces extends React.Component {
saveWorkspace = (id: WorkspaceId) => { saveWorkspace = (id: WorkspaceId) => {
const draft = toJS(this.editingWorkspaces.get(id)); const draft = toJS(this.editingWorkspaces.get(id));
const workspace = workspaceStore.saveWorkspace(draft); const workspace = workspaceStore.addWorkspace(draft);
if (workspace) { if (workspace) {
this.clearEditing(id); this.clearEditing(id);
} }
@ -50,11 +54,11 @@ export class Workspaces extends React.Component {
addWorkspace = () => { addWorkspace = () => {
const workspaceId = uuid(); const workspaceId = uuid();
this.editingWorkspaces.set(workspaceId, { this.editingWorkspaces.set(workspaceId, new Workspace({
id: workspaceId, id: workspaceId,
name: "", name: "",
description: "", description: ""
}) }))
} }
editWorkspace = (id: WorkspaceId) => { editWorkspace = (id: WorkspaceId) => {
@ -76,7 +80,7 @@ export class Workspaces extends React.Component {
}, },
ok: () => { ok: () => {
this.clearEditing(id); this.clearEditing(id);
workspaceStore.removeWorkspace(id); workspaceStore.removeWorkspace(workspace);
}, },
message: ( message: (
<div className="confirm flex column gaps"> <div className="confirm flex column gaps">
@ -107,11 +111,12 @@ export class Workspaces extends React.Component {
<Trans>Workspaces</Trans> <Trans>Workspaces</Trans>
</h2> </h2>
<div className="items flex column gaps"> <div className="items flex column gaps">
{this.workspaces.map(({ id: workspaceId, name, description }) => { {this.workspaces.map(({ id: workspaceId, name, description, ownerRef }) => {
const isActive = workspaceStore.currentWorkspaceId === workspaceId; const isActive = workspaceStore.currentWorkspaceId === workspaceId;
const isDefault = workspaceStore.isDefault(workspaceId); const isDefault = workspaceStore.isDefault(workspaceId);
const isEditing = this.editingWorkspaces.has(workspaceId); const isEditing = this.editingWorkspaces.has(workspaceId);
const editingWorkspace = this.editingWorkspaces.get(workspaceId); const editingWorkspace = this.editingWorkspaces.get(workspaceId);
const managed = !!ownerRef
const className = cssNames("workspace flex gaps", { const className = cssNames("workspace flex gaps", {
active: isActive, active: isActive,
editing: isEditing, editing: isEditing,
@ -130,7 +135,7 @@ export class Workspaces extends React.Component {
{isActive && <span> <Trans>(current)</Trans></span>} {isActive && <span> <Trans>(current)</Trans></span>}
</span> </span>
<span className="description">{description}</span> <span className="description">{description}</span>
{!isDefault && ( {!isDefault && !managed && (
<Fragment> <Fragment>
<Icon <Icon
material="edit" material="edit"

View File

@ -101,9 +101,11 @@ export class ClustersMenu extends React.Component<Props> {
} }
render() { render() {
const { className } = this.props; const { className } = this.props
const { newContexts } = userStore; const { newContexts } = userStore
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId); const workspace = workspaceStore.getById(workspaceStore.currentWorkspaceId)
const clusters = clusterStore.getByWorkspaceId(workspace.id)
const activeClusterId = clusterStore.activeCluster
return ( return (
<div className={cssNames("ClustersMenu flex column", className)}> <div className={cssNames("ClustersMenu flex column", className)}>
<div className="clusters flex column gaps"> <div className="clusters flex column gaps">
@ -112,7 +114,7 @@ export class ClustersMenu extends React.Component<Props> {
{({ innerRef, droppableProps, placeholder }: DroppableProvided) => ( {({ innerRef, droppableProps, placeholder }: DroppableProvided) => (
<div ref={innerRef} {...droppableProps}> <div ref={innerRef} {...droppableProps}>
{clusters.map((cluster, index) => { {clusters.map((cluster, index) => {
const isActive = cluster.id === clusterStore.activeClusterId; const isActive = cluster.id === activeClusterId;
return ( return (
<Draggable draggableId={cluster.id} index={index} key={cluster.id}> <Draggable draggableId={cluster.id} index={index} key={cluster.id}>
{({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => ( {({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => (
@ -136,11 +138,11 @@ export class ClustersMenu extends React.Component<Props> {
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
</div> </div>
<div className="add-cluster" onClick={this.addCluster}> <div className="add-cluster" >
<Tooltip targetId="add-cluster-icon"> <Tooltip targetId="add-cluster-icon">
<Trans>Add Cluster</Trans> <Trans>Add Cluster</Trans>
</Tooltip> </Tooltip>
<Icon big material="add" id="add-cluster-icon"/> <Icon big material="add" id="add-cluster-icon" disabled={workspace.isManaged} onClick={this.addCluster}/>
{newContexts.size > 0 && ( {newContexts.size > 0 && (
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>}/> <Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>}/>
)} )}