diff --git a/extensions/support-page/package-lock.json b/extensions/support-page/package-lock.json index 029ee4a2eb..babb7a254a 100644 --- a/extensions/support-page/package-lock.json +++ b/extensions/support-page/package-lock.json @@ -20,6 +20,12 @@ "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==", "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": { "version": "14.11.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.11.tgz", @@ -741,6 +747,12 @@ "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": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -842,6 +854,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "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": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -980,6 +998,71 @@ "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": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz", @@ -1600,6 +1683,12 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "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": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", @@ -1618,6 +1707,12 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "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": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -1810,6 +1905,33 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "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": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", @@ -2051,6 +2173,12 @@ "dev": 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": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -2317,6 +2445,71 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "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": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -2576,6 +2769,58 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -2882,6 +3127,49 @@ "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": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -3053,6 +3341,12 @@ "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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", diff --git a/extensions/support-page/package.json b/extensions/support-page/package.json index 2ed40c6fa8..a556eeb37b 100644 --- a/extensions/support-page/package.json +++ b/extensions/support-page/package.json @@ -10,13 +10,16 @@ }, "dependencies": {}, "devDependencies": { + "@k8slens/extensions": "file:../../src/extensions/npm/extensions", "@types/node": "^14.11.11", "@types/react": "^16.9.53", "@types/react-router": "^5.1.8", "@types/webpack": "^4.41.17", - "@k8slens/extensions": "file:../../src/extensions/npm/extensions", + "css-loader": "^5.0.0", "mobx": "^5.15.5", "react": "^16.13.1", + "sass-loader": "^10.0.4", + "style-loader": "^2.0.0", "ts-loader": "^8.0.4", "ts-node": "^9.0.0", "typescript": "^4.0.3", diff --git a/extensions/support-page/src/support.scss b/extensions/support-page/src/support.scss new file mode 100644 index 0000000000..efc14f2827 --- /dev/null +++ b/extensions/support-page/src/support.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/extensions/support-page/src/support.tsx b/extensions/support-page/src/support.tsx index 37c42021b2..6c588a1cef 100644 --- a/extensions/support-page/src/support.tsx +++ b/extensions/support-page/src/support.tsx @@ -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 +import "./support.scss" import React from "react" import { observer } from "mobx-react" import { App, Component } from "@k8slens/extensions"; diff --git a/extensions/support-page/webpack.config.ts b/extensions/support-page/webpack.config.ts index c7e5e56cb0..45ca3d2a10 100644 --- a/extensions/support-page/webpack.config.ts +++ b/extensions/support-page/webpack.config.ts @@ -2,7 +2,6 @@ import path from "path" const outputPath = path.resolve(__dirname, 'dist'); -// TODO: figure out how to share base TS and Webpack configs from Lens (npm, filesystem, etc?) const lensExternals = { "@k8slens/extensions": "var global.LensExtensions", "react": "var global.React", @@ -50,6 +49,14 @@ export default [ use: 'ts-loader', exclude: /node_modules/, }, + { + test: /\.s?css$/, + use: [ + "style-loader", + "css-loader", + "sass-loader", + ] + } ], }, externals: [ diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 724a83f0d7..8c911298cc 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -64,13 +64,13 @@ describe("empty config", () => { it("sets active cluster", () => { clusterStore.setActive("foo"); - expect(clusterStore.activeCluster.id).toBe("foo"); + expect(clusterStore.active.id).toBe("foo"); }) }) describe("with prod and dev clusters added", () => { beforeEach(() => { - clusterStore.addCluster( + clusterStore.addClusters( new Cluster({ id: "prod", contextName: "prod", diff --git a/src/common/__tests__/workspace-store.test.ts b/src/common/__tests__/workspace-store.test.ts index 8ac3ac599d..97edfa77cf 100644 --- a/src/common/__tests__/workspace-store.test.ts +++ b/src/common/__tests__/workspace-store.test.ts @@ -10,7 +10,7 @@ jest.mock("electron", () => { } }) -import { WorkspaceStore } from "../workspace-store" +import { Workspace, WorkspaceStore } from "../workspace-store" describe("workspace store tests", () => { describe("for an empty config", () => { @@ -35,16 +35,16 @@ describe("workspace store tests", () => { it("cannot remove the default workspace", () => { const ws = WorkspaceStore.getInstance(); - expect(() => ws.removeWorkspace(WorkspaceStore.defaultId)).toThrowError("Cannot remove"); + expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove"); }) it("can update default workspace name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: WorkspaceStore.defaultId, name: "foobar", - }); + })); expect(ws.currentWorkspace.name).toBe("foobar"); }) @@ -52,10 +52,10 @@ describe("workspace store tests", () => { it("can add workspaces", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "123", name: "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", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "abc", name: "foobar", - }); + })); expect(() => ws.setActive("abc")).not.toThrowError(); }) @@ -80,15 +80,15 @@ describe("workspace store tests", () => { it("can remove a workspace", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "123", name: "foobar", - }); - ws.saveWorkspace({ + })); + ws.addWorkspace(new Workspace({ id: "1234", name: "foobar 1", - }); - ws.removeWorkspace("123"); + })); + ws.removeWorkspaceById("123"); expect(ws.workspaces.size).toBe(2); }) @@ -96,10 +96,10 @@ describe("workspace store tests", () => { it("cannot create workspace with existent name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "someid", name: "default", - }); + })); expect(ws.workspacesList.length).toBe(1); // default workspace only }) @@ -107,10 +107,10 @@ describe("workspace store tests", () => { it("cannot create workspace with empty name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "random", name: "", - }); + })); expect(ws.workspacesList.length).toBe(1); // default workspace only }) @@ -118,10 +118,10 @@ describe("workspace store tests", () => { it("cannot create workspace with ' ' name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "random", name: " ", - }); + })); expect(ws.workspacesList.length).toBe(1); // default workspace only }) @@ -129,10 +129,10 @@ describe("workspace store tests", () => { it("trim workspace name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "random", name: "default ", - }); + })); expect(ws.workspacesList.length).toBe(1); // default workspace only }) @@ -169,4 +169,4 @@ describe("workspace store tests", () => { expect(ws.currentWorkspaceId).toBe("abc"); }) }) -}) \ No newline at end of file +}) diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index fddd0f3be6..838e6cc119 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -33,11 +33,12 @@ export type ClusterId = string; export interface ClusterModel { id: ClusterId; + kubeConfigPath: string; workspace?: WorkspaceId; contextName?: string; preferences?: ClusterPreferences; metadata?: ClusterMetadata; - kubeConfigPath: string; + ownerRef?: string; /** @deprecated */ kubeConfig?: string; // yaml @@ -72,25 +73,34 @@ export class ClusterStore extends BaseStore { return filePath; } + @observable activeCluster: ClusterId; + @observable removedClusters = observable.map(); + @observable clusters = observable.map(); + private constructor() { super({ configName: "lens-cluster-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names migrations: migrations, }); + + this.pushStateToViewsPeriodically() } - @observable activeClusterId: ClusterId; - @observable removedClusters = observable.map(); - @observable clusters = observable.map(); + protected pushStateToViewsPeriodically() { + if (!ipcRenderer) { + // 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() { logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`) - ipcRenderer.on("cluster:state", (event, model: ClusterState) => { - this.applyWithoutSync(() => { - logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, model); - this.getById(model.id)?.updateModel(model); - }) + ipcRenderer.on("cluster:state", (event, clusterId: string, state: ClusterState) => { + logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state); + this.getById(clusterId)?.setState(state) }) } @@ -99,21 +109,35 @@ export class ClusterStore extends BaseStore { ipcRenderer.removeAllListeners("cluster:state") } - @computed get activeCluster(): Cluster | null { - return this.getById(this.activeClusterId); + pushState() { + this.clusters.forEach((c) => { + c.pushState() + }) + } + + get activeClusterId() { + return this.activeCluster } @computed get clustersList(): Cluster[] { 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) { - return this.activeClusterId === id; + return this.activeCluster === id; } @action setActive(id: ClusterId) { - this.activeClusterId = this.clusters.has(id) ? id : null; + this.activeCluster = this.clusters.has(id) ? id : null; } @action @@ -145,12 +169,28 @@ export class ClusterStore extends BaseStore { } @action - addCluster(...models: ClusterModel[]) { + addClusters(...models: ClusterModel[]): Cluster[] { + const clusters: Cluster[] = [] models.forEach(model => { - appEventBus.emit({name: "cluster", action: "add"}) - const cluster = new Cluster(model); - this.clusters.set(model.id, cluster); + clusters.push(this.addCluster(model)) }) + + 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 @@ -159,7 +199,7 @@ export class ClusterStore extends BaseStore { const cluster = this.getById(clusterId); if (cluster) { this.clusters.delete(clusterId); - if (this.activeClusterId === clusterId) { + if (this.activeCluster === clusterId) { this.setActive(null); } // remove only custom kubeconfigs (pasted as text) @@ -189,6 +229,9 @@ export class ClusterStore extends BaseStore { cluster.updateModel(clusterModel); } else { cluster = new Cluster(clusterModel); + if (!cluster.isManaged) { + cluster.enabled = true + } } newClusters.set(clusterModel.id, cluster); } @@ -200,14 +243,14 @@ export class ClusterStore extends BaseStore { } }); - this.activeClusterId = newClusters.has(activeCluster) ? activeCluster : null; + this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null; this.clusters.replace(newClusters); this.removedClusters.replace(removedClusters); } toJSON(): ClusterStoreModel { return toJS({ - activeCluster: this.activeClusterId, + activeCluster: this.activeCluster, clusters: this.clustersList.map(cluster => cluster.toJSON()), }, { recurseEverything: true diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index b7f2467013..97611a01d3 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -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 { clusterStore } from "./cluster-store" import { appEventBus } from "./event-bus"; +import { broadcastIpc } from "../common/ipc"; +import logger from "../main/logger"; export type WorkspaceId = string; export interface WorkspaceStoreModel { currentWorkspace?: WorkspaceId; - workspaces: Workspace[] + workspaces: WorkspaceModel[] } -export interface Workspace { +export interface WorkspaceModel { id: WorkspaceId; name: 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 { @@ -23,15 +81,33 @@ export class WorkspaceStore extends BaseStore { super({ 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 workspaces = observable.map({ - [WorkspaceStore.defaultId]: { + [WorkspaceStore.defaultId]: new Workspace({ id: WorkspaceStore.defaultId, name: "default" - } + }) }); @computed get currentWorkspace(): Workspace { @@ -42,6 +118,16 @@ export class WorkspaceStore extends BaseStore { 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) { return id === WorkspaceStore.defaultId; } @@ -61,11 +147,11 @@ export class WorkspaceStore extends BaseStore { throw new Error(`workspace ${id} doesn't exist`); } 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 - saveWorkspace(workspace: Workspace) { + addWorkspace(workspace: Workspace) { const { id, name } = workspace; const existingWorkspace = this.getById(id); if (!name.trim() || this.getByName(name.trim())) { @@ -82,7 +168,12 @@ export class WorkspaceStore extends BaseStore { } @action - removeWorkspace(id: WorkspaceId) { + removeWorkspace(workspace: Workspace) { + this.removeWorkspaceById(workspace.id) + } + + @action + removeWorkspaceById(id: WorkspaceId) { const workspace = this.getById(id); if (!workspace) return; if (this.isDefault(id)) { @@ -103,7 +194,11 @@ export class WorkspaceStore extends BaseStore { } if (workspaces.length) { 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) }) } @@ -112,7 +207,7 @@ export class WorkspaceStore extends BaseStore { toJSON(): WorkspaceStoreModel { return toJS({ currentWorkspace: this.currentWorkspaceId, - workspaces: this.workspacesList, + workspaces: this.workspacesList.map((w) => w.toJSON()), }, { recurseEverything: true }) diff --git a/src/extensions/core-api/stores.ts b/src/extensions/core-api/stores.ts index dfaae325af..d39314f762 100644 --- a/src/extensions/core-api/stores.ts +++ b/src/extensions/core-api/stores.ts @@ -1,4 +1,4 @@ export { ExtensionStore } from "../extension-store" export { clusterStore, ClusterModel } from "../../common/cluster-store" -export { workspaceStore} from "../../common/workspace-store" -export type { Cluster } from "../../main/cluster" +export { Cluster } from "../../main/cluster" +export { workspaceStore, Workspace, WorkspaceModel } from "../../common/workspace-store" diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 21556180b4..0620356e9a 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -10,7 +10,7 @@ export class ClusterManager { constructor(public readonly port: number) { // auto-init clusters autorun(() => { - clusterStore.clusters.forEach(cluster => { + clusterStore.enabledClustersList.forEach(cluster => { if (!cluster.initialized) { logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta()); cluster.init(port); diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 11964ba319..57b9e219c3 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,3 +1,4 @@ +import { ipcMain } from "electron" import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { WorkspaceId } from "../common/workspace-store"; @@ -33,7 +34,7 @@ export type ClusterRefreshOptions = { refreshMetadata?: boolean } -export interface ClusterState extends ClusterModel { +export interface ClusterState { initialized: boolean; apiUrl: string; online: boolean; @@ -47,11 +48,12 @@ export interface ClusterState extends ClusterModel { allowedResources: string[] } -export class Cluster implements ClusterModel { +export class Cluster implements ClusterModel, ClusterState { public id: ClusterId; public frameId: number; public kubeCtl: Kubectl public contextHandler: ContextHandler; + public ownerRef: string; protected kubeconfigManager: KubeconfigManager; protected eventDisposers: Function[] = []; protected activated = false; @@ -65,6 +67,7 @@ export class Cluster implements ClusterModel { @observable kubeConfigPath: string; @observable apiUrl: string; // cluster server url @observable kubeProxyUrl: string; // lens-proxy to kube-api url + @observable enabled = false; @observable online = false; @observable accessible = false; @observable ready = false; @@ -81,6 +84,7 @@ export class Cluster implements ClusterModel { @computed get available() { return this.accessible && !this.disconnected; } + get version(): string { return String(this.metadata?.version) || "" } @@ -93,6 +97,10 @@ export class Cluster implements ClusterModel { } } + get isManaged(): boolean { + return !!this.ownerRef + } + @action updateModel(model: ClusterModel) { Object.assign(this, model); @@ -123,13 +131,15 @@ export class Cluster implements ClusterModel { const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes - this.eventDisposers.push( - reaction(this.getState, this.pushState), - () => { - clearInterval(refreshTimer); - clearInterval(refreshMetadataTimer); - }, - ); + if (ipcMain) { + this.eventDisposers.push( + reaction(() => this.getState(), () => this.pushState()), + () => { + clearInterval(refreshTimer); + clearInterval(refreshMetadataTimer); + }, + ); + } } protected unbindEvents() { @@ -361,6 +371,7 @@ export class Cluster implements ClusterModel { workspace: this.workspace, preferences: this.preferences, metadata: this.metadata, + ownerRef: this.ownerRef }; return toJS(model, { recurseEverything: true @@ -368,9 +379,8 @@ export class Cluster implements ClusterModel { } // serializable cluster-state used for sync btw main <-> renderer - getState = (): ClusterState => { + getState(): ClusterState { const state: ClusterState = { - ...this.toJSON(), initialized: this.initialized, apiUrl: this.apiUrl, 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); broadcastIpc({ channel: "cluster:state", frameId: this.frameId, - args: [state], - }); - return state; + args: [this.id, state], + }) } // get cluster system meta, e.g. use in "logger" diff --git a/src/main/tray.ts b/src/main/tray.ts index 31cc99b314..428fd5cb3d 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -80,7 +80,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu { }, { label: "Clusters", - submenu: workspaceStore.workspacesList + submenu: workspaceStore.enabledWorkspacesList .filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces .map(workspace => { const clusters = clusterStore.getByWorkspaceId(workspace.id); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 5d92dd624d..7311e0e0cf 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -40,6 +40,7 @@ export async function bootstrap(App: AppComponent) { // Register additional store listeners clusterStore.registerIpcListener(); + workspaceStore.registerIpcListener(); // init app's dependencies if any if (App.init) { diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 0751dd2d8c..8acd3a51ea 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -163,7 +163,7 @@ export class AddCluster extends React.Component { }) runInAction(() => { - clusterStore.addCluster(...newClusters); + clusterStore.addClusters(...newClusters); if (newClusters.length === 1) { const clusterId = newClusters[0].id; clusterStore.setActive(clusterId); diff --git a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx index 6cd933ca11..ea4ee5a571 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx @@ -26,11 +26,11 @@ export class ClusterWorkspaceSetting extends React.Component {