mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into implement-kube-api-patch
This commit is contained in:
commit
bc207fa0db
14
.github/workflows/stale.yml
vendored
Normal file
14
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
name: Cull Stale Issues
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@4
|
||||||
|
with:
|
||||||
|
only-issue-labels: "needs-information"
|
||||||
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
# -1 here means that PRs will never be marked as stale
|
||||||
|
days-before-pr-stale: -1
|
||||||
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
73
.idea/codeStyles/Project.xml
generated
Normal file
73
.idea/codeStyles/Project.xml
generated
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<option name="AUTODETECT_INDENTS" value="false" />
|
||||||
|
<option name="RIGHT_MARGIN" value="100" />
|
||||||
|
<HTMLCodeStyleSettings>
|
||||||
|
<option name="HTML_ATTRIBUTE_WRAP" value="4" />
|
||||||
|
<option name="HTML_TEXT_WRAP" value="0" />
|
||||||
|
<option name="HTML_KEEP_BLANK_LINES" value="1" />
|
||||||
|
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||||
|
<option name="HTML_DO_NOT_INDENT_CHILDREN_OF" value="" />
|
||||||
|
<option name="HTML_ENFORCE_QUOTES" value="true" />
|
||||||
|
</HTMLCodeStyleSettings>
|
||||||
|
<JSCodeStyleSettings version="0">
|
||||||
|
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||||
|
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||||
|
<option name="OBJECT_LITERAL_WRAP" value="2" />
|
||||||
|
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||||
|
<option name="IMPORT_USE_NODE_RESOLUTION" value="FALSE" />
|
||||||
|
</JSCodeStyleSettings>
|
||||||
|
<MarkdownNavigatorCodeStyleSettings>
|
||||||
|
<option name="RIGHT_MARGIN" value="72" />
|
||||||
|
</MarkdownNavigatorCodeStyleSettings>
|
||||||
|
<TypeScriptCodeStyleSettings version="0">
|
||||||
|
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||||
|
</TypeScriptCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="HTML">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="JavaScript">
|
||||||
|
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||||
|
<option name="CATCH_ON_NEW_LINE" value="true" />
|
||||||
|
<option name="FINALLY_ON_NEW_LINE" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||||
|
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||||
|
<option name="CALL_PARAMETERS_WRAP" value="5" />
|
||||||
|
<option name="PREFER_PARAMETERS_WRAP" value="true" />
|
||||||
|
<option name="METHOD_PARAMETERS_WRAP" value="5" />
|
||||||
|
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="METHOD_CALL_CHAIN_WRAP" value="5" />
|
||||||
|
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="TERNARY_OPERATION_WRAP" value="5" />
|
||||||
|
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="ARRAY_INITIALIZER_WRAP" value="5" />
|
||||||
|
<option name="ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="ARRAY_INITIALIZER_RBRACE_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="ASSIGNMENT_WRAP" value="5" />
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="TypeScript">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
6
.idea/encodings.xml
generated
Normal file
6
.idea/encodings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding">
|
||||||
|
<file url="PROJECT" charset="UTF-8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/jsLinters/eslint.xml
generated
Normal file
6
.idea/jsLinters/eslint.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EslintConfiguration">
|
||||||
|
<option name="fix-on-save" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
19
.idea/lens.iml
generated
Normal file
19
.idea/lens.iml
generated
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/node_modules" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/extensions/kube-object-event-status/node_modules" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/extensions/metrics-cluster-feature/node_modules" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/extensions/node-menu/node_modules" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/extensions/pod-menu/node_modules" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/static/build" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/src/extensions/npm/extensions/dist" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="WebPackConfiguration">
|
||||||
|
<option name="mode" value="DISABLED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/lens.iml" filepath="$PROJECT_DIR$/.idea/lens.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/prettier.xml
generated
Normal file
6
.idea/prettier.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PrettierConfiguration">
|
||||||
|
<option name="myRunOnReformat" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
14
.idea/webResources.xml
generated
Normal file
14
.idea/webResources.xml
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="WebResourcesPaths">
|
||||||
|
<contentEntries>
|
||||||
|
<entry url="file://$PROJECT_DIR$">
|
||||||
|
<entryData>
|
||||||
|
<resourceRoots>
|
||||||
|
<path value="file://$PROJECT_DIR$" />
|
||||||
|
</resourceRoots>
|
||||||
|
</entryData>
|
||||||
|
</entry>
|
||||||
|
</contentEntries>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -61,14 +61,14 @@ export class MetricsSettings extends React.Component<Props> {
|
|||||||
persistence: {
|
persistence: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
storageClass: null,
|
storageClass: null,
|
||||||
size: "20G",
|
size: "20GiB",
|
||||||
},
|
},
|
||||||
nodeExporter: {
|
nodeExporter: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
retention: {
|
retention: {
|
||||||
time: "2d",
|
time: "2d",
|
||||||
size: "5GB",
|
size: "5GiB",
|
||||||
},
|
},
|
||||||
kubeStateMetrics: {
|
kubeStateMetrics: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@ -331,6 +331,14 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
|
|||||||
await cleanup();
|
await cleanup();
|
||||||
}, 10*60*1000);
|
}, 10*60*1000);
|
||||||
|
|
||||||
|
it("shows cluster context menu in sidebar", async () => {
|
||||||
|
await frame.click(`[data-testid="sidebar-cluster-dropdown"]`);
|
||||||
|
await frame.waitForSelector(`.Menu >> text="Add to Hotbar"`);
|
||||||
|
await frame.waitForSelector(`.Menu >> text="Settings"`);
|
||||||
|
await frame.waitForSelector(`.Menu >> text="Disconnect"`);
|
||||||
|
await frame.waitForSelector(`.Menu >> text="Delete"`);
|
||||||
|
});
|
||||||
|
|
||||||
it("should navigate around common cluster pages", async () => {
|
it("should navigate around common cluster pages", async () => {
|
||||||
for (const test of commonPageTests) {
|
for (const test of commonPageTests) {
|
||||||
if (isTopPageTest(test)) {
|
if (isTopPageTest(test)) {
|
||||||
@ -362,6 +370,8 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
|
|||||||
}
|
}
|
||||||
}, 10*60*1000);
|
}, 10*60*1000);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
it("show logs and highlight the log search entries", async () => {
|
it("show logs and highlight the log search entries", async () => {
|
||||||
await frame.click(`a[href="/workloads"]`);
|
await frame.click(`a[href="/workloads"]`);
|
||||||
await frame.click(`a[href="/pods"]`);
|
await frame.click(`a[href="/pods"]`);
|
||||||
|
|||||||
12
package.json
12
package.json
@ -3,7 +3,7 @@
|
|||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
"homepage": "https://github.com/lensapp/lens",
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "5.3.0-alpha.9",
|
"version": "5.3.0-alpha.10",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -220,7 +220,7 @@
|
|||||||
"moment-timezone": "^0.5.33",
|
"moment-timezone": "^0.5.33",
|
||||||
"monaco-editor": "^0.29.1",
|
"monaco-editor": "^0.29.1",
|
||||||
"monaco-editor-webpack-plugin": "^5.0.0",
|
"monaco-editor-webpack-plugin": "^5.0.0",
|
||||||
"node-fetch": "^2.6.6",
|
"node-fetch": "lensapp/node-fetch#2.x",
|
||||||
"node-pty": "^0.10.1",
|
"node-pty": "^0.10.1",
|
||||||
"npm": "^6.14.15",
|
"npm": "^6.14.15",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
@ -254,7 +254,7 @@
|
|||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||||
"@sentry/types": "^6.13.3",
|
"@sentry/types": "^6.14.1",
|
||||||
"@testing-library/jest-dom": "^5.15.0",
|
"@testing-library/jest-dom": "^5.15.0",
|
||||||
"@testing-library/react": "^11.2.7",
|
"@testing-library/react": "^11.2.7",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
@ -278,13 +278,13 @@
|
|||||||
"@types/md5-file": "^4.0.2",
|
"@types/md5-file": "^4.0.2",
|
||||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "14.17.27",
|
"@types/node": "14.17.33",
|
||||||
"@types/node-fetch": "^2.5.12",
|
"@types/node-fetch": "^2.5.12",
|
||||||
"@types/npm": "^2.0.32",
|
"@types/npm": "^2.0.32",
|
||||||
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
||||||
"@types/proper-lockfile": "^4.1.2",
|
"@types/proper-lockfile": "^4.1.2",
|
||||||
"@types/randomcolor": "^0.5.6",
|
"@types/randomcolor": "^0.5.6",
|
||||||
"@types/react": "^17.0.33",
|
"@types/react": "^17.0.34",
|
||||||
"@types/react-beautiful-dnd": "^13.1.2",
|
"@types/react-beautiful-dnd": "^13.1.2",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-dom": "^17.0.9",
|
||||||
"@types/react-router-dom": "^5.3.2",
|
"@types/react-router-dom": "^5.3.2",
|
||||||
@ -361,7 +361,7 @@
|
|||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"sharp": "^0.29.2",
|
"sharp": "^0.29.2",
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^2.0.0",
|
||||||
"tailwindcss": "^2.2.17",
|
"tailwindcss": "^2.2.19",
|
||||||
"ts-jest": "26.5.6",
|
"ts-jest": "26.5.6",
|
||||||
"ts-loader": "^7.0.5",
|
"ts-loader": "^7.0.5",
|
||||||
"ts-node": "^10.4.0",
|
"ts-node": "^10.4.0",
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request } from "node-fetch";
|
import type { Request } from "node-fetch";
|
||||||
import { Pod } from "../endpoints/pods.api";
|
import { Pod, PodsApi } from "../endpoints/pods.api";
|
||||||
import { forRemoteCluster, KubeApi } from "../kube-api";
|
import { forRemoteCluster, KubeApi } from "../kube-api";
|
||||||
import { KubeJsonApi } from "../kube-json-api";
|
import { KubeJsonApi } from "../kube-json-api";
|
||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
@ -34,7 +34,33 @@ class TestKubeObject extends KubeObject {
|
|||||||
class TestKubeApi extends KubeApi<TestKubeObject> {}
|
class TestKubeApi extends KubeApi<TestKubeObject> {}
|
||||||
|
|
||||||
describe("forRemoteCluster", () => {
|
describe("forRemoteCluster", () => {
|
||||||
it("builds api client", async (done) => {
|
it("builds api client for KubeObject", async () => {
|
||||||
|
const api = forRemoteCluster({
|
||||||
|
cluster: {
|
||||||
|
server: "https://127.0.0.1:6443",
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
token: "daa",
|
||||||
|
},
|
||||||
|
}, Pod);
|
||||||
|
|
||||||
|
expect(api).toBeInstanceOf(KubeApi);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds api client for given KubeApi", async () => {
|
||||||
|
const api = forRemoteCluster({
|
||||||
|
cluster: {
|
||||||
|
server: "https://127.0.0.1:6443",
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
token: "daa",
|
||||||
|
},
|
||||||
|
}, Pod, PodsApi);
|
||||||
|
|
||||||
|
expect(api).toBeInstanceOf(PodsApi);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls right api endpoint", async () => {
|
||||||
const api = forRemoteCluster({
|
const api = forRemoteCluster({
|
||||||
cluster: {
|
cluster: {
|
||||||
server: "https://127.0.0.1:6443",
|
server: "https://127.0.0.1:6443",
|
||||||
@ -47,13 +73,13 @@ describe("forRemoteCluster", () => {
|
|||||||
(fetch as any).mockResponse(async (request: any) => {
|
(fetch as any).mockResponse(async (request: any) => {
|
||||||
expect(request.url).toEqual("https://127.0.0.1:6443/api/v1/pods");
|
expect(request.url).toEqual("https://127.0.0.1:6443/api/v1/pods");
|
||||||
|
|
||||||
done();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
body: "",
|
body: "hello",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect.hasAssertions();
|
||||||
|
|
||||||
await api.list();
|
await api.list();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { IMetrics, metricsApi } from "./metrics.api";
|
|||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
import type { KubeJsonApiData } from "../kube-json-api";
|
import type { KubeJsonApiData } from "../kube-json-api";
|
||||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||||
|
import type { RequireExactlyOne } from "type-fest";
|
||||||
|
|
||||||
export class IngressApi extends KubeApi<Ingress> {
|
export class IngressApi extends KubeApi<Ingress> {
|
||||||
}
|
}
|
||||||
@ -58,7 +59,7 @@ export interface ILoadBalancerIngress {
|
|||||||
// extensions/v1beta1
|
// extensions/v1beta1
|
||||||
interface IExtensionsBackend {
|
interface IExtensionsBackend {
|
||||||
serviceName: string;
|
serviceName: string;
|
||||||
servicePort: number;
|
servicePort: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// networking.k8s.io/v1
|
// networking.k8s.io/v1
|
||||||
@ -101,14 +102,18 @@ export interface Ingress {
|
|||||||
}[];
|
}[];
|
||||||
// extensions/v1beta1
|
// extensions/v1beta1
|
||||||
backend?: IExtensionsBackend;
|
backend?: IExtensionsBackend;
|
||||||
// networking.k8s.io/v1
|
/**
|
||||||
defaultBackend?: INetworkingBackend & {
|
* The default backend which is exactly on of:
|
||||||
|
* - service
|
||||||
|
* - resource
|
||||||
|
*/
|
||||||
|
defaultBackend?: RequireExactlyOne<INetworkingBackend & {
|
||||||
resource: {
|
resource: {
|
||||||
apiGroup: string;
|
apiGroup: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
}
|
}>
|
||||||
};
|
};
|
||||||
status: {
|
status: {
|
||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
@ -153,10 +158,11 @@ export class Ingress extends KubeObject {
|
|||||||
return routes;
|
return routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
getServiceNamePort() {
|
getServiceNamePort(): IExtensionsBackend {
|
||||||
const { spec } = this;
|
const { spec: { backend, defaultBackend } = {}} = this;
|
||||||
const serviceName = spec?.defaultBackend?.service.name ?? spec?.backend?.serviceName;
|
|
||||||
const servicePort = spec?.defaultBackend?.service.port.number ?? spec?.defaultBackend?.service.port.name ?? spec?.backend?.servicePort;
|
const serviceName = defaultBackend?.service?.name ?? backend?.serviceName;
|
||||||
|
const servicePort = defaultBackend?.service?.port.number ?? defaultBackend?.service?.port.name ?? backend?.servicePort;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
serviceName,
|
serviceName,
|
||||||
|
|||||||
@ -236,13 +236,10 @@ export class Node extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getOperatingSystem(): string {
|
getOperatingSystem(): string {
|
||||||
const label = this.getLabels().find(label => label.startsWith("kubernetes.io/os="));
|
return this.metadata?.labels?.["kubernetes.io/os"]
|
||||||
|
|| this.metadata?.labels?.["beta.kubernetes.io/os"]
|
||||||
if (label) {
|
|| this.status?.nodeInfo?.operatingSystem
|
||||||
return label.split("=", 2)[1];
|
|| "linux";
|
||||||
}
|
|
||||||
|
|
||||||
return "linux";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isUnschedulable() {
|
isUnschedulable() {
|
||||||
|
|||||||
@ -165,40 +165,40 @@ interface IContainerProbe {
|
|||||||
failureThreshold?: number;
|
failureThreshold?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContainerStateRunning {
|
||||||
|
startedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContainerStateWaiting {
|
||||||
|
reason: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContainerStateTerminated {
|
||||||
|
startedAt: string;
|
||||||
|
finishedAt: string;
|
||||||
|
exitCode: number;
|
||||||
|
reason: string;
|
||||||
|
containerID?: string;
|
||||||
|
message?: string;
|
||||||
|
signal?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ContainerState holds a possible state of container. Only one of its members
|
||||||
|
* may be specified. If none of them is specified, the default one is
|
||||||
|
* `ContainerStateWaiting`.
|
||||||
|
*/
|
||||||
|
export interface ContainerState {
|
||||||
|
running?: ContainerStateRunning;
|
||||||
|
waiting?: ContainerStateWaiting;
|
||||||
|
terminated?: ContainerStateTerminated;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IPodContainerStatus {
|
export interface IPodContainerStatus {
|
||||||
name: string;
|
name: string;
|
||||||
state?: {
|
state?: ContainerState;
|
||||||
[index: string]: object;
|
lastState?: ContainerState;
|
||||||
running?: {
|
|
||||||
startedAt: string;
|
|
||||||
};
|
|
||||||
waiting?: {
|
|
||||||
reason: string;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
terminated?: {
|
|
||||||
startedAt: string;
|
|
||||||
finishedAt: string;
|
|
||||||
exitCode: number;
|
|
||||||
reason: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
lastState?: {
|
|
||||||
[index: string]: object;
|
|
||||||
running?: {
|
|
||||||
startedAt: string;
|
|
||||||
};
|
|
||||||
waiting?: {
|
|
||||||
reason: string;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
terminated?: {
|
|
||||||
startedAt: string;
|
|
||||||
finishedAt: string;
|
|
||||||
exitCode: number;
|
|
||||||
reason: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
restartCount: number;
|
restartCount: number;
|
||||||
image: string;
|
image: string;
|
||||||
@ -373,23 +373,16 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns pod phase or container error if occurred
|
// Returns pod phase or container error if occurred
|
||||||
getStatusMessage() {
|
getStatusMessage(): string {
|
||||||
if (this.getReason() === PodStatus.EVICTED) return "Evicted";
|
if (this.getReason() === PodStatus.EVICTED) {
|
||||||
if (this.metadata.deletionTimestamp) return "Terminating";
|
return "Evicted";
|
||||||
|
|
||||||
const statuses = this.getContainerStatuses(false); // not including initContainers
|
|
||||||
|
|
||||||
for (const { state } of statuses.reverse()) {
|
|
||||||
if (state.waiting) {
|
|
||||||
return state.waiting.reason || "Waiting";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.terminated) {
|
|
||||||
return state.terminated.reason || "Terminated";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getStatusPhase();
|
if (this.metadata.deletionTimestamp) {
|
||||||
|
return "Terminating";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getStatusPhase() || "Waiting";
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatusPhase() {
|
getStatusPhase() {
|
||||||
|
|||||||
@ -116,7 +116,7 @@ export interface IRemoteKubeApiConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function forCluster<T extends KubeObject>(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor<T>): KubeApi<T> {
|
export function forCluster<T extends KubeObject, Y extends KubeApi<T> = KubeApi<T>>(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor<T>, apiClass: new (apiOpts: IKubeApiOptions<T>) => Y = null): KubeApi<T> {
|
||||||
const url = new URL(apiBase.config.serverAddress);
|
const url = new URL(apiBase.config.serverAddress);
|
||||||
const request = new KubeJsonApi({
|
const request = new KubeJsonApi({
|
||||||
serverAddress: apiBase.config.serverAddress,
|
serverAddress: apiBase.config.serverAddress,
|
||||||
@ -128,15 +128,18 @@ export function forCluster<T extends KubeObject>(cluster: ILocalKubeApiConfig, k
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return new KubeApi({
|
if (!apiClass) {
|
||||||
|
apiClass = KubeApi as new (apiOpts: IKubeApiOptions<T>) => Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new apiClass({
|
||||||
objectConstructor: kubeClass,
|
objectConstructor: kubeClass,
|
||||||
request,
|
request,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function forRemoteCluster<T extends KubeObject>(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor<T>): KubeApi<T> {
|
export function forRemoteCluster<T extends KubeObject, Y extends KubeApi<T> = KubeApi<T>>(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor<T>, apiClass: new (apiOpts: IKubeApiOptions<T>) => Y = null): Y {
|
||||||
const reqInit: RequestInit = {};
|
const reqInit: RequestInit = {};
|
||||||
|
|
||||||
const agentOptions: AgentOptions = {};
|
const agentOptions: AgentOptions = {};
|
||||||
|
|
||||||
if (config.cluster.skipTLSVerify === true) {
|
if (config.cluster.skipTLSVerify === true) {
|
||||||
@ -173,8 +176,12 @@ export function forRemoteCluster<T extends KubeObject>(config: IRemoteKubeApiCon
|
|||||||
} : {}),
|
} : {}),
|
||||||
}, reqInit);
|
}, reqInit);
|
||||||
|
|
||||||
return new KubeApi({
|
if (!apiClass) {
|
||||||
objectConstructor: kubeClass,
|
apiClass = KubeApi as new (apiOpts: IKubeApiOptions<T>) => Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new apiClass({
|
||||||
|
objectConstructor: kubeClass as KubeObjectConstructor<T>,
|
||||||
request,
|
request,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -233,10 +240,7 @@ export class KubeApi<T extends KubeObject> {
|
|||||||
isNamespaced = options.objectConstructor?.namespaced,
|
isNamespaced = options.objectConstructor?.namespaced,
|
||||||
} = options || {};
|
} = options || {};
|
||||||
|
|
||||||
if (!options.apiBase) {
|
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase || objectConstructor.apiBase);
|
||||||
options.apiBase = objectConstructor.apiBase;
|
|
||||||
}
|
|
||||||
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase);
|
|
||||||
|
|
||||||
this.kind = kind;
|
this.kind = kind;
|
||||||
this.isNamespaced = isNamespaced;
|
this.isNamespaced = isNamespaced;
|
||||||
|
|||||||
@ -21,6 +21,9 @@
|
|||||||
|
|
||||||
import { JsonApi, JsonApiData, JsonApiError } from "./json-api";
|
import { JsonApi, JsonApiData, JsonApiError } from "./json-api";
|
||||||
import type { Response } from "node-fetch";
|
import type { Response } from "node-fetch";
|
||||||
|
import type { Cluster } from "../../main/cluster";
|
||||||
|
import { LensProxy } from "../../main/lens-proxy";
|
||||||
|
import { apiKubePrefix, isDebugging } from "../vars";
|
||||||
|
|
||||||
export interface KubeJsonApiListMetadata {
|
export interface KubeJsonApiListMetadata {
|
||||||
resourceVersion: string;
|
resourceVersion: string;
|
||||||
@ -70,6 +73,20 @@ export interface KubeJsonApiError extends JsonApiError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class KubeJsonApi extends JsonApi<KubeJsonApiData> {
|
export class KubeJsonApi extends JsonApi<KubeJsonApiData> {
|
||||||
|
static forCluster(cluster: Cluster): KubeJsonApi {
|
||||||
|
const port = LensProxy.getInstance().port;
|
||||||
|
|
||||||
|
return new this({
|
||||||
|
serverAddress: `http://127.0.0.1:${port}`,
|
||||||
|
apiBase: apiKubePrefix,
|
||||||
|
debug: isDebugging,
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
"Host": `${cluster.id}.localhost:${port}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected parseError(error: KubeJsonApiError | any, res: Response): string[] {
|
protected parseError(error: KubeJsonApiError | any, res: Response): string[] {
|
||||||
const { status, reason, message } = error;
|
const { status, reason, message } = error;
|
||||||
|
|
||||||
@ -80,4 +97,3 @@ export class KubeJsonApi extends JsonApi<KubeJsonApiData> {
|
|||||||
return super.parseError(error, res);
|
return super.parseError(error, res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,11 +23,20 @@
|
|||||||
import * as Base64 from "crypto-js/enc-base64";
|
import * as Base64 from "crypto-js/enc-base64";
|
||||||
import * as Utf8 from "crypto-js/enc-utf8";
|
import * as Utf8 from "crypto-js/enc-utf8";
|
||||||
|
|
||||||
export const base64 = {
|
/**
|
||||||
decode(data: string) {
|
* Computes utf-8 from base64
|
||||||
return Base64.parse(data).toString(Utf8);
|
* @param data A Base64 encoded string
|
||||||
},
|
* @returns The original utf-8 string
|
||||||
encode(data: string) {
|
*/
|
||||||
return Utf8.parse(data).toString(Base64);
|
export function decode(data: string): string {
|
||||||
},
|
return Base64.parse(data).toString(Utf8);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes base64 from utf-8
|
||||||
|
* @param data A normal string
|
||||||
|
* @returns A base64 encoded version
|
||||||
|
*/
|
||||||
|
export function encode(data: string): string {
|
||||||
|
return Utf8.parse(data).toString(Base64);
|
||||||
|
}
|
||||||
|
|||||||
@ -28,7 +28,6 @@ export function noop<T extends any[]>(...args: T): void {
|
|||||||
|
|
||||||
export * from "./app-version";
|
export * from "./app-version";
|
||||||
export * from "./autobind";
|
export * from "./autobind";
|
||||||
export * from "./base64";
|
|
||||||
export * from "./camelCase";
|
export * from "./camelCase";
|
||||||
export * from "./cloneJson";
|
export * from "./cloneJson";
|
||||||
export * from "./cluster-id-url-parsing";
|
export * from "./cluster-id-url-parsing";
|
||||||
@ -62,9 +61,11 @@ export * from "./types";
|
|||||||
import * as iter from "./iter";
|
import * as iter from "./iter";
|
||||||
import * as array from "./array";
|
import * as array from "./array";
|
||||||
import * as tuple from "./tuple";
|
import * as tuple from "./tuple";
|
||||||
|
import * as base64 from "./base64";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
iter,
|
iter,
|
||||||
array,
|
array,
|
||||||
tuple,
|
tuple,
|
||||||
|
base64,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx";
|
import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx";
|
||||||
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityKindData } from "../../common/catalog";
|
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityConstructor, CatalogEntityKindData } from "../../common/catalog";
|
||||||
import { iter } from "../../common/utils";
|
import { iter } from "../../common/utils";
|
||||||
|
|
||||||
export class CatalogEntityRegistry {
|
export class CatalogEntityRegistry {
|
||||||
@ -59,7 +59,7 @@ export class CatalogEntityRegistry {
|
|||||||
return this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind) as T[];
|
return this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind) as T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemsByEntityClass<T extends CatalogEntity>({ apiVersion, kind }: CatalogEntityKindData): T[] {
|
getItemsByEntityClass<T extends CatalogEntity>({ apiVersion, kind }: CatalogEntityKindData & CatalogEntityConstructor<T>): T[] {
|
||||||
return this.getItemsForApiKind(apiVersion, kind);
|
return this.getItemsForApiKind(apiVersion, kind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,8 @@ export class ClusterManager extends Singleton {
|
|||||||
private store = ClusterStore.getInstance();
|
private store = ClusterStore.getInstance();
|
||||||
deleting = observable.set<ClusterId>();
|
deleting = observable.set<ClusterId>();
|
||||||
|
|
||||||
|
@observable visibleCluster: ClusterId | undefined = undefined;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
@ -61,8 +63,22 @@ export class ClusterManager extends Singleton {
|
|||||||
{ fireImmediately: false },
|
{ fireImmediately: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
|
reaction(
|
||||||
this.syncClustersFromCatalog(entities);
|
() => catalogEntityRegistry.getItemsByEntityClass(KubernetesCluster),
|
||||||
|
entities => this.syncClustersFromCatalog(entities),
|
||||||
|
);
|
||||||
|
|
||||||
|
reaction(() => [
|
||||||
|
catalogEntityRegistry.getItemsByEntityClass(KubernetesCluster),
|
||||||
|
this.visibleCluster,
|
||||||
|
] as const, ([entities, visibleCluster]) => {
|
||||||
|
for (const entity of entities) {
|
||||||
|
if (entity.getId() === visibleCluster) {
|
||||||
|
entity.status.active = true;
|
||||||
|
} else {
|
||||||
|
entity.status.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
observe(this.deleting, change => {
|
observe(this.deleting, change => {
|
||||||
@ -240,27 +256,20 @@ export class ClusterManager extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getClusterForRequest(req: http.IncomingMessage): Cluster {
|
getClusterForRequest(req: http.IncomingMessage): Cluster {
|
||||||
let cluster: Cluster = null;
|
|
||||||
|
|
||||||
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
||||||
if (req.headers.host.startsWith("127.0.0.1")) {
|
if (req.headers.host.startsWith("127.0.0.1")) {
|
||||||
const clusterId = req.url.split("/")[1];
|
const clusterId = req.url.split("/")[1];
|
||||||
|
const cluster = this.store.getById(clusterId);
|
||||||
cluster = this.store.getById(clusterId);
|
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
// we need to swap path prefix so that request is proxied to kube api
|
// we need to swap path prefix so that request is proxied to kube api
|
||||||
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
|
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
|
||||||
}
|
}
|
||||||
} else if (req.headers["x-cluster-id"]) {
|
|
||||||
cluster = this.store.getById(req.headers["x-cluster-id"].toString());
|
|
||||||
} else {
|
|
||||||
const clusterId = getClusterIdFromHost(req.headers.host);
|
|
||||||
|
|
||||||
cluster = this.store.getById(clusterId);
|
return cluster;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cluster;
|
return this.store.getById(getClusterIdFromHost(req.headers.host));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -63,8 +63,9 @@ import { ensureDir } from "fs-extra";
|
|||||||
import { Router } from "./router";
|
import { Router } from "./router";
|
||||||
import { initMenu } from "./menu";
|
import { initMenu } from "./menu";
|
||||||
import { initTray } from "./tray";
|
import { initTray } from "./tray";
|
||||||
import { kubeApiRequest, shellApiRequest } from "./proxy-functions";
|
import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions";
|
||||||
import { AppPaths } from "../common/app-paths";
|
import { AppPaths } from "../common/app-paths";
|
||||||
|
import { ShellSession } from "./shell-session/shell-session";
|
||||||
|
|
||||||
injectSystemCAs();
|
injectSystemCAs();
|
||||||
|
|
||||||
@ -122,7 +123,7 @@ app.on("second-instance", (event, argv) => {
|
|||||||
WindowManager.getInstance(false)?.ensureMainWindow();
|
WindowManager.getInstance(false)?.ensureMainWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("ready", async () => {
|
app.on("ready", async () => {
|
||||||
logger.info(`🚀 Starting ${productName} from "${AppPaths.get("exe")}"`);
|
logger.info(`🚀 Starting ${productName} from "${AppPaths.get("exe")}"`);
|
||||||
logger.info("🐚 Syncing shell environment");
|
logger.info("🐚 Syncing shell environment");
|
||||||
await shellSync();
|
await shellSync();
|
||||||
@ -134,6 +135,7 @@ app.on("ready", async () => {
|
|||||||
registerFileProtocol("static", __static);
|
registerFileProtocol("static", __static);
|
||||||
|
|
||||||
PrometheusProviderRegistry.createInstance();
|
PrometheusProviderRegistry.createInstance();
|
||||||
|
ShellRequestAuthenticator.createInstance().init();
|
||||||
initializers.initPrometheusProviderRegistry();
|
initializers.initPrometheusProviderRegistry();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -227,6 +229,7 @@ app.on("ready", async () => {
|
|||||||
onQuitCleanup.push(
|
onQuitCleanup.push(
|
||||||
initMenu(windowManager),
|
initMenu(windowManager),
|
||||||
initTray(windowManager),
|
initTray(windowManager),
|
||||||
|
() => ShellSession.cleanup(),
|
||||||
);
|
);
|
||||||
|
|
||||||
installDeveloperTools();
|
installDeveloperTools();
|
||||||
|
|||||||
@ -20,13 +20,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { BrowserWindow, dialog, IpcMainInvokeEvent } from "electron";
|
import { BrowserWindow, dialog, IpcMainInvokeEvent } from "electron";
|
||||||
import { KubernetesCluster } from "../../common/catalog-entities";
|
|
||||||
import { clusterFrameMap } from "../../common/cluster-frames";
|
import { clusterFrameMap } from "../../common/cluster-frames";
|
||||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../common/cluster-ipc";
|
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../common/cluster-ipc";
|
||||||
import type { ClusterId } from "../../common/cluster-types";
|
import type { ClusterId } from "../../common/cluster-types";
|
||||||
import { ClusterStore } from "../../common/cluster-store";
|
import { ClusterStore } from "../../common/cluster-store";
|
||||||
import { appEventBus } from "../../common/event-bus";
|
import { appEventBus } from "../../common/event-bus";
|
||||||
import { dialogShowOpenDialogHandler, ipcMainHandle } from "../../common/ipc";
|
import { dialogShowOpenDialogHandler, ipcMainHandle, ipcMainOn } from "../../common/ipc";
|
||||||
import { catalogEntityRegistry } from "../catalog";
|
import { catalogEntityRegistry } from "../catalog";
|
||||||
import { pushCatalogToRenderer } from "../catalog-pusher";
|
import { pushCatalogToRenderer } from "../catalog-pusher";
|
||||||
import { ClusterManager } from "../cluster-manager";
|
import { ClusterManager } from "../cluster-manager";
|
||||||
@ -54,16 +53,8 @@ export function initIpcMainHandlers() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMainHandle(clusterVisibilityHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId, visible: boolean) => {
|
ipcMainOn(clusterVisibilityHandler, (event, clusterId?: ClusterId) => {
|
||||||
const entity = catalogEntityRegistry.getById(clusterId);
|
ClusterManager.getInstance().visibleCluster = clusterId;
|
||||||
|
|
||||||
for (const kubeEntity of catalogEntityRegistry.getItemsByEntityClass(KubernetesCluster)) {
|
|
||||||
kubeEntity.status.active = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entity) {
|
|
||||||
entity.status.active = visible;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMainHandle(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
ipcMainHandle(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
||||||
|
|||||||
@ -60,12 +60,10 @@ export class LensProxy extends Singleton {
|
|||||||
|
|
||||||
public port: number;
|
public port: number;
|
||||||
|
|
||||||
constructor(protected router: Router, functions: LensProxyFunctions) {
|
constructor(protected router: Router, { shellApiRequest, kubeApiRequest, getClusterForRequest }: LensProxyFunctions) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
const { shellApiRequest, kubeApiRequest } = functions;
|
this.getClusterForRequest = getClusterForRequest;
|
||||||
|
|
||||||
this.getClusterForRequest = functions.getClusterForRequest;
|
|
||||||
|
|
||||||
this.proxyServer = spdy.createServer({
|
this.proxyServer = spdy.createServer({
|
||||||
spdy: {
|
spdy: {
|
||||||
@ -79,10 +77,16 @@ export class LensProxy extends Singleton {
|
|||||||
this.proxyServer
|
this.proxyServer
|
||||||
.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
|
.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
|
||||||
const isInternal = req.url.startsWith(`${apiPrefix}?`);
|
const isInternal = req.url.startsWith(`${apiPrefix}?`);
|
||||||
|
const cluster = getClusterForRequest(req);
|
||||||
|
|
||||||
|
if (!cluster) {
|
||||||
|
return void logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`);
|
||||||
|
}
|
||||||
|
|
||||||
const reqHandler = isInternal ? shellApiRequest : kubeApiRequest;
|
const reqHandler = isInternal ? shellApiRequest : kubeApiRequest;
|
||||||
|
|
||||||
(async () => reqHandler({ req, socket, head }))()
|
(async () => reqHandler({ req, socket, head, cluster }))()
|
||||||
.catch(error => logger.error(logger.error(`[LENS-PROXY]: failed to handle proxy upgrade: ${error}`)));
|
.catch(error => logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +146,10 @@ export class LensProxy extends Singleton {
|
|||||||
res.flushHeaders();
|
res.flushHeaders();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proxyRes.on("aborted", () => { // happens when proxy target aborts connection
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
proxy.on("error", (error, req, res, target) => {
|
proxy.on("error", (error, req, res, target) => {
|
||||||
|
|||||||
@ -23,18 +23,11 @@ import { chunk } from "lodash";
|
|||||||
import net from "net";
|
import net from "net";
|
||||||
import url from "url";
|
import url from "url";
|
||||||
import { apiKubePrefix } from "../../common/vars";
|
import { apiKubePrefix } from "../../common/vars";
|
||||||
import { ClusterManager } from "../cluster-manager";
|
|
||||||
import type { ProxyApiRequestArgs } from "./types";
|
import type { ProxyApiRequestArgs } from "./types";
|
||||||
|
|
||||||
const skipRawHeaders = new Set(["Host", "Authorization"]);
|
const skipRawHeaders = new Set(["Host", "Authorization"]);
|
||||||
|
|
||||||
export async function kubeApiRequest({ req, socket, head }: ProxyApiRequestArgs) {
|
export async function kubeApiRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) {
|
||||||
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
|
||||||
|
|
||||||
if (!cluster) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
||||||
const apiUrl = url.parse(cluster.apiUrl);
|
const apiUrl = url.parse(cluster.apiUrl);
|
||||||
const pUrl = url.parse(proxyUrl);
|
const pUrl = url.parse(proxyUrl);
|
||||||
|
|||||||
@ -19,29 +19,81 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type http from "http";
|
|
||||||
import url from "url";
|
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import * as WebSocket from "ws";
|
import { Server as WebSocketServer } from "ws";
|
||||||
import { NodeShellSession, LocalShellSession } from "../shell-session";
|
import { NodeShellSession, LocalShellSession } from "../shell-session";
|
||||||
import type { ProxyApiRequestArgs } from "./types";
|
import type { ProxyApiRequestArgs } from "./types";
|
||||||
import { ClusterManager } from "../cluster-manager";
|
import { ClusterManager } from "../cluster-manager";
|
||||||
|
import URLParse from "url-parse";
|
||||||
|
import { ExtendedMap, Singleton } from "../../common/utils";
|
||||||
|
import type { ClusterId } from "../../common/cluster-types";
|
||||||
|
import { ipcMainHandle } from "../../common/ipc";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
export function shellApiRequest({ req, socket, head }: ProxyApiRequestArgs) {
|
const randomBytes = promisify(crypto.randomBytes);
|
||||||
const ws = new WebSocket.Server({ noServer: true });
|
|
||||||
|
|
||||||
ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => {
|
export class ShellRequestAuthenticator extends Singleton {
|
||||||
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
private tokens = new ExtendedMap<ClusterId, Map<string, Uint8Array>>();
|
||||||
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
|
|
||||||
const shell = nodeParam
|
init() {
|
||||||
? new NodeShellSession(socket, cluster, nodeParam)
|
ipcMainHandle("cluster:shell-api", async (event, clusterId, tabId) => {
|
||||||
: new LocalShellSession(socket, cluster);
|
const authToken = Uint8Array.from(await randomBytes(128));
|
||||||
|
|
||||||
|
this.tokens
|
||||||
|
.getOrInsert(clusterId, () => new Map())
|
||||||
|
.set(tabId, authToken);
|
||||||
|
|
||||||
|
return authToken;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticates a single use token for creating a new shell
|
||||||
|
* @param clusterId The `ClusterId` for the shell
|
||||||
|
* @param tabId The ID for the shell
|
||||||
|
* @param token The value that is being presented as a one time authentication token
|
||||||
|
* @returns `true` if `token` was valid, false otherwise
|
||||||
|
*/
|
||||||
|
authenticate(clusterId: ClusterId, tabId: string, token: string): boolean {
|
||||||
|
const clusterTokens = this.tokens.get(clusterId);
|
||||||
|
|
||||||
|
if (!clusterTokens) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authToken = clusterTokens.get(tabId);
|
||||||
|
const buf = Uint8Array.from(Buffer.from(token, "base64"));
|
||||||
|
|
||||||
|
if (authToken instanceof Uint8Array && authToken.length === buf.length && crypto.timingSafeEqual(authToken, buf)) {
|
||||||
|
// remove the token because it is a single use token
|
||||||
|
clusterTokens.delete(tabId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shellApiRequest({ req, socket, head }: ProxyApiRequestArgs): void {
|
||||||
|
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
||||||
|
const { query: { node, shellToken, id: tabId }} = new URLParse(req.url, true);
|
||||||
|
|
||||||
|
if (!cluster || !ShellRequestAuthenticator.getInstance().authenticate(cluster.id, tabId, shellToken)) {
|
||||||
|
socket.write("Invalid shell request");
|
||||||
|
|
||||||
|
return void socket.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ws = new WebSocketServer({ noServer: true });
|
||||||
|
|
||||||
|
ws.handleUpgrade(req, socket, head, (webSocket) => {
|
||||||
|
const shell = node
|
||||||
|
? new NodeShellSession(webSocket, cluster, node, tabId)
|
||||||
|
: new LocalShellSession(webSocket, cluster, tabId);
|
||||||
|
|
||||||
shell.open()
|
shell.open()
|
||||||
.catch(error => logger.error(`[SHELL-SESSION]: failed to open: ${error}`, { error }));
|
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${node ? "node" : "local"} shell`, error));
|
||||||
}));
|
|
||||||
|
|
||||||
ws.handleUpgrade(req, socket, head, (con) => {
|
|
||||||
ws.emit("connection", con, req);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,9 +21,11 @@
|
|||||||
|
|
||||||
import type http from "http";
|
import type http from "http";
|
||||||
import type net from "net";
|
import type net from "net";
|
||||||
|
import type { Cluster } from "../cluster";
|
||||||
|
|
||||||
export interface ProxyApiRequestArgs {
|
export interface ProxyApiRequestArgs {
|
||||||
req: http.IncomingMessage,
|
req: http.IncomingMessage,
|
||||||
socket: net.Socket,
|
socket: net.Socket,
|
||||||
head: Buffer,
|
head: Buffer,
|
||||||
|
cluster: Cluster,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,7 +95,7 @@ class PortForward {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.process.stderr.on("data", (data) => {
|
this.process.stderr.on("data", (data) => {
|
||||||
logger.warn(`[PORT-FORWARD-ROUTE]: kubectl port-forward process stderr: ${data}`);
|
logger.debug(`[PORT-FORWARD-ROUTE]: kubectl port-forward process stderr: ${data}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const internalPort = await getPortFrom(this.process.stdout, {
|
const internalPort = await getPortFrom(this.process.stdout, {
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export class LocalShellSession extends ShellSession {
|
|||||||
const shell = env.PTYSHELL;
|
const shell = env.PTYSHELL;
|
||||||
const args = await this.getShellArgs(shell);
|
const args = await this.getShellArgs(shell);
|
||||||
|
|
||||||
super.open(env.PTYSHELL, args, env);
|
await this.openShellProcess(env.PTYSHELL, args, env);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getShellArgs(shell: string): Promise<string[]> {
|
protected async getShellArgs(shell: string): Promise<string[]> {
|
||||||
|
|||||||
@ -19,26 +19,27 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as WebSocket from "ws";
|
import type WebSocket from "ws";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import * as k8s from "@kubernetes/client-node";
|
import * as k8s from "@kubernetes/client-node";
|
||||||
import type { KubeConfig } from "@kubernetes/client-node";
|
import type { KubeConfig } from "@kubernetes/client-node";
|
||||||
import type { Cluster } from "../cluster";
|
import type { Cluster } from "../cluster";
|
||||||
import { ShellOpenError, ShellSession } from "./shell-session";
|
import { ShellOpenError, ShellSession } from "./shell-session";
|
||||||
import { get } from "lodash";
|
import { get } from "lodash";
|
||||||
|
import { Node, NodesApi } from "../../common/k8s-api/endpoints";
|
||||||
|
import { KubeJsonApi } from "../../common/k8s-api/kube-json-api";
|
||||||
|
import logger from "../logger";
|
||||||
|
|
||||||
export class NodeShellSession extends ShellSession {
|
export class NodeShellSession extends ShellSession {
|
||||||
ShellType = "node-shell";
|
ShellType = "node-shell";
|
||||||
|
|
||||||
protected podId = `node-shell-${uuid()}`;
|
protected readonly podName = `node-shell-${uuid()}`;
|
||||||
protected kc: KubeConfig;
|
protected kc: KubeConfig;
|
||||||
|
|
||||||
protected get cwd(): string | undefined {
|
protected readonly cwd: string | undefined = undefined;
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string) {
|
constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string, terminalId: string) {
|
||||||
super(socket, cluster);
|
super(socket, cluster, terminalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async open() {
|
public async open() {
|
||||||
@ -55,10 +56,27 @@ export class NodeShellSession extends ShellSession {
|
|||||||
throw new ShellOpenError("failed to create node pod", error);
|
throw new ShellOpenError("failed to create node pod", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"];
|
|
||||||
const env = await this.getCachedShellEnv();
|
const env = await this.getCachedShellEnv();
|
||||||
|
const args = ["exec", "-i", "-t", "-n", "kube-system", this.podName, "--"];
|
||||||
|
const nodeApi = new NodesApi({
|
||||||
|
objectConstructor: Node,
|
||||||
|
request: KubeJsonApi.forCluster(this.cluster),
|
||||||
|
});
|
||||||
|
const node = await nodeApi.get({ name: this.nodeName });
|
||||||
|
const nodeOs = node.getOperatingSystem();
|
||||||
|
|
||||||
await super.open(shell, args, env);
|
switch (nodeOs) {
|
||||||
|
default:
|
||||||
|
logger.warn(`[NODE-SHELL-SESSION]: could not determine node OS, falling back with assumption of linux`);
|
||||||
|
case "linux":
|
||||||
|
args.push("sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))");
|
||||||
|
break;
|
||||||
|
case "windows":
|
||||||
|
args.push("powershell");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.openShellProcess(shell, args, env);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createNodeShellPod() {
|
protected createNodeShellPod() {
|
||||||
@ -73,7 +91,7 @@ export class NodeShellSession extends ShellSession {
|
|||||||
.makeApiClient(k8s.CoreV1Api)
|
.makeApiClient(k8s.CoreV1Api)
|
||||||
.createNamespacedPod("kube-system", {
|
.createNamespacedPod("kube-system", {
|
||||||
metadata: {
|
metadata: {
|
||||||
name: this.podId,
|
name: this.podName,
|
||||||
namespace: "kube-system",
|
namespace: "kube-system",
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
@ -101,33 +119,39 @@ export class NodeShellSession extends ShellSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected waitForRunningPod(): Promise<void> {
|
protected waitForRunningPod(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
logger.debug(`[NODE-SHELL]: waiting for ${this.podName} to be running`);
|
||||||
const watch = new k8s.Watch(this.kc);
|
|
||||||
|
|
||||||
watch
|
return new Promise((resolve, reject) => {
|
||||||
|
new k8s.Watch(this.kc)
|
||||||
.watch(`/api/v1/namespaces/kube-system/pods`,
|
.watch(`/api/v1/namespaces/kube-system/pods`,
|
||||||
{},
|
{},
|
||||||
// callback is called for each received object.
|
// callback is called for each received object.
|
||||||
(type, obj) => {
|
(type, { metadata: { name }, status }) => {
|
||||||
if (obj.metadata.name == this.podId && obj.status.phase === "Running") {
|
if (name === this.podName) {
|
||||||
resolve();
|
switch (status.phase) {
|
||||||
|
case "Running":
|
||||||
|
return resolve();
|
||||||
|
case "Failed":
|
||||||
|
return reject(`Failed to be created: ${status.message || "unknown error"}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// done callback is called if the watch terminates normally
|
// done callback is called if the watch terminates normally
|
||||||
(err) => {
|
(err) => {
|
||||||
console.log(err);
|
logger.error(`[NODE-SHELL]: ${this.podName} was not created in time`);
|
||||||
reject(err);
|
reject(err);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then(req => {
|
.then(req => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log("aborting");
|
logger.error(`[NODE-SHELL]: aborting wait for ${this.podName}, timing out`);
|
||||||
req.abort();
|
req.abort();
|
||||||
}, 2 * 60 * 1000);
|
reject("Pod creation timed out");
|
||||||
|
}, 2 * 60 * 1000); // 2 * 60 * 1000
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(error => {
|
||||||
console.log("watch failed");
|
logger.error(`[NODE-SHELL]: waiting for ${this.podName} failed: ${error}`);
|
||||||
reject(err);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -141,6 +165,7 @@ export class NodeShellSession extends ShellSession {
|
|||||||
this
|
this
|
||||||
.kc
|
.kc
|
||||||
.makeApiClient(k8s.CoreV1Api)
|
.makeApiClient(k8s.CoreV1Api)
|
||||||
.deleteNamespacedPod(this.podId, "kube-system");
|
.deleteNamespacedPod(this.podName, "kube-system")
|
||||||
|
.catch(error => logger.warn(`[NODE-SHELL]: failed to remove pod shell`, error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import type { Cluster } from "../cluster";
|
import type { Cluster } from "../cluster";
|
||||||
import { Kubectl } from "../kubectl";
|
import { Kubectl } from "../kubectl";
|
||||||
import type * as WebSocket from "ws";
|
import type WebSocket from "ws";
|
||||||
import { shellEnv } from "../utils/shell-env";
|
import { shellEnv } from "../utils/shell-env";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
|
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
|
||||||
@ -31,6 +31,7 @@ import { isWindows } from "../../common/vars";
|
|||||||
import { UserStore } from "../../common/user-store";
|
import { UserStore } from "../../common/user-store";
|
||||||
import * as pty from "node-pty";
|
import * as pty from "node-pty";
|
||||||
import { appEventBus } from "../../common/event-bus";
|
import { appEventBus } from "../../common/event-bus";
|
||||||
|
import logger from "../logger";
|
||||||
|
|
||||||
export class ShellOpenError extends Error {
|
export class ShellOpenError extends Error {
|
||||||
constructor(message: string, public cause: Error) {
|
constructor(message: string, public cause: Error) {
|
||||||
@ -40,41 +41,140 @@ export class ShellOpenError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum WebSocketCloseEvent {
|
||||||
|
/**
|
||||||
|
* The connection successfully completed the purpose for which it was created.
|
||||||
|
*/
|
||||||
|
NormalClosure = 1000,
|
||||||
|
/**
|
||||||
|
* The endpoint is going away, either because of a server failure or because
|
||||||
|
* the browser is navigating away from the page that opened the connection.
|
||||||
|
*/
|
||||||
|
GoingAway = 1001,
|
||||||
|
/**
|
||||||
|
* The endpoint is terminating the connection due to a protocol error.
|
||||||
|
*/
|
||||||
|
ProtocolError = 1002,
|
||||||
|
/**
|
||||||
|
* The connection is being terminated because the endpoint received data of a
|
||||||
|
* type it cannot accept. (For example, a text-only endpoint received binary
|
||||||
|
* data.)
|
||||||
|
*/
|
||||||
|
UnsupportedData = 1003,
|
||||||
|
/**
|
||||||
|
* Indicates that no status code was provided even though one was expected.
|
||||||
|
*/
|
||||||
|
NoStatusReceived = 1005,
|
||||||
|
/**
|
||||||
|
* Indicates that a connection was closed abnormally (that is, with no close
|
||||||
|
* frame being sent) when a status code is expected.
|
||||||
|
*/
|
||||||
|
AbnormalClosure = 1006,
|
||||||
|
/**
|
||||||
|
* The endpoint is terminating the connection because a message was received
|
||||||
|
* that contained inconsistent data (e.g., non-UTF-8 data within a text message).
|
||||||
|
*/
|
||||||
|
InvalidFramePayloadData = 1007,
|
||||||
|
/**
|
||||||
|
* The endpoint is terminating the connection because it received a message
|
||||||
|
* that violates its policy. This is a generic status code, used when codes
|
||||||
|
* 1003 and 1009 are not suitable.
|
||||||
|
*/
|
||||||
|
PolicyViolation = 1008,
|
||||||
|
/**
|
||||||
|
* The endpoint is terminating the connection because a data frame was
|
||||||
|
* received that is too large.
|
||||||
|
*/
|
||||||
|
MessageTooBig = 1009,
|
||||||
|
/**
|
||||||
|
* The client is terminating the connection because it expected the server to
|
||||||
|
* negotiate one or more extension, but the server didn't.
|
||||||
|
*/
|
||||||
|
MissingExtension = 1010,
|
||||||
|
/**
|
||||||
|
* The server is terminating the connection because it encountered an
|
||||||
|
* unexpected condition that prevented it from fulfilling the request.
|
||||||
|
*/
|
||||||
|
InternalError = 1011,
|
||||||
|
/**
|
||||||
|
* The server is terminating the connection because it is restarting.
|
||||||
|
*/
|
||||||
|
ServiceRestart = 1012,
|
||||||
|
/**
|
||||||
|
* The server is terminating the connection due to a temporary condition,
|
||||||
|
* e.g. it is overloaded and is casting off some of its clients.
|
||||||
|
*/
|
||||||
|
TryAgainLater = 1013,
|
||||||
|
/**
|
||||||
|
* The server was acting as a gateway or proxy and received an invalid
|
||||||
|
* response from the upstream server. This is similar to 502 HTTP Status Code.
|
||||||
|
*/
|
||||||
|
BadGateway = 1014,
|
||||||
|
/**
|
||||||
|
* Indicates that the connection was closed due to a failure to perform a TLS
|
||||||
|
* handshake (e.g., the server certificate can't be verified).
|
||||||
|
*/
|
||||||
|
TlsHandshake = 1015,
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class ShellSession {
|
export abstract class ShellSession {
|
||||||
abstract ShellType: string;
|
abstract ShellType: string;
|
||||||
|
|
||||||
static shellEnvs: Map<string, Record<string, any>> = new Map();
|
private static shellEnvs = new Map<string, Record<string, string>>();
|
||||||
|
private static processes = new Map<string, pty.IPty>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kill all remaining shell backing processes. Should be called when about to
|
||||||
|
* quit
|
||||||
|
*/
|
||||||
|
public static cleanup(): void {
|
||||||
|
for (const shellProcess of this.processes.values()) {
|
||||||
|
try {
|
||||||
|
process.kill(shellProcess.pid);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
protected kubectl: Kubectl;
|
protected kubectl: Kubectl;
|
||||||
protected running = false;
|
protected running = false;
|
||||||
protected shellProcess: pty.IPty;
|
|
||||||
protected kubectlBinDirP: Promise<string>;
|
protected kubectlBinDirP: Promise<string>;
|
||||||
protected kubeconfigPathP: Promise<string>;
|
protected kubeconfigPathP: Promise<string>;
|
||||||
|
protected readonly terminalId: string;
|
||||||
|
|
||||||
protected abstract get cwd(): string | undefined;
|
protected abstract get cwd(): string | undefined;
|
||||||
|
|
||||||
constructor(protected websocket: WebSocket, protected cluster: Cluster) {
|
protected ensureShellProcess(shell: string, args: string[], env: Record<string, string>, cwd: string): pty.IPty {
|
||||||
|
if (!ShellSession.processes.has(this.terminalId)) {
|
||||||
|
ShellSession.processes.set(this.terminalId, pty.spawn(shell, args, {
|
||||||
|
cols: 80,
|
||||||
|
cwd,
|
||||||
|
env,
|
||||||
|
name: "xterm-256color",
|
||||||
|
rows: 30,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ShellSession.processes.get(this.terminalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(protected websocket: WebSocket, protected cluster: Cluster, terminalId: string) {
|
||||||
this.kubectl = new Kubectl(cluster.version);
|
this.kubectl = new Kubectl(cluster.version);
|
||||||
this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath();
|
this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath();
|
||||||
this.kubectlBinDirP = this.kubectl.binDir();
|
this.kubectlBinDirP = this.kubectl.binDir();
|
||||||
|
this.terminalId = `${cluster.id}:${terminalId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async open(shell: string, args: string[], env: Record<string, any>) {
|
protected async openShellProcess(shell: string, args: string[], env: Record<string, any>) {
|
||||||
const cwd = (this.cwd && await fse.pathExists(this.cwd))
|
const cwd = (this.cwd && await fse.pathExists(this.cwd))
|
||||||
? this.cwd
|
? this.cwd
|
||||||
: env.HOME;
|
: env.HOME;
|
||||||
|
const shellProcess = this.ensureShellProcess(shell, args, env, cwd);
|
||||||
|
|
||||||
this.shellProcess = pty.spawn(shell, args, {
|
|
||||||
cols: 80,
|
|
||||||
cwd,
|
|
||||||
env,
|
|
||||||
name: "xterm-256color",
|
|
||||||
rows: 30,
|
|
||||||
});
|
|
||||||
this.running = true;
|
this.running = true;
|
||||||
|
shellProcess.onData(data => this.sendResponse(data));
|
||||||
this.shellProcess.onData(data => this.sendResponse(data));
|
shellProcess.onExit(({ exitCode }) => {
|
||||||
this.shellProcess.onExit(({ exitCode }) => {
|
|
||||||
this.running = false;
|
this.running = false;
|
||||||
|
|
||||||
if (exitCode > 0) {
|
if (exitCode > 0) {
|
||||||
@ -95,24 +195,28 @@ export abstract class ShellSession {
|
|||||||
|
|
||||||
switch (data[0]) {
|
switch (data[0]) {
|
||||||
case "0":
|
case "0":
|
||||||
this.shellProcess.write(message);
|
shellProcess.write(message);
|
||||||
break;
|
break;
|
||||||
case "4":
|
case "4":
|
||||||
const { Width, Height } = JSON.parse(message);
|
const { Width, Height } = JSON.parse(message);
|
||||||
|
|
||||||
this.shellProcess.resize(Width, Height);
|
shellProcess.resize(Width, Height);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("close", () => {
|
.on("close", (code) => {
|
||||||
if (this.running) {
|
logger.debug(`[SHELL-SESSION]: websocket for ${this.terminalId} closed with code=${code}`);
|
||||||
|
|
||||||
|
if (this.running && code !== WebSocketCloseEvent.AbnormalClosure) {
|
||||||
|
// This code is the one that gets sent when the network is turned off
|
||||||
try {
|
try {
|
||||||
process.kill(this.shellProcess.pid);
|
logger.info(`[SHELL-SESSION]: Killing shell process for ${this.terminalId}`);
|
||||||
|
process.kill(shellProcess.pid);
|
||||||
|
ShellSession.processes.delete(this.terminalId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
}
|
}
|
||||||
|
this.running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.running = false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
appEventBus.emit({ name: this.ShellType, action: "open" });
|
appEventBus.emit({ name: this.ShellType, action: "open" });
|
||||||
@ -191,7 +295,7 @@ export abstract class ShellSession {
|
|||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected exit(code = 1000) {
|
protected exit(code = WebSocketCloseEvent.NormalClosure) {
|
||||||
if (this.websocket.readyState == this.websocket.OPEN) {
|
if (this.websocket.readyState == this.websocket.OPEN) {
|
||||||
this.websocket.close(code);
|
this.websocket.close(code);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,13 +19,13 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { boundMethod, base64, EventEmitter } from "../utils";
|
import { boundMethod, base64, EventEmitter, getHostedClusterId } from "../utils";
|
||||||
import { WebSocketApi } from "./websocket-api";
|
import { WebSocketApi } from "./websocket-api";
|
||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
import { isDevelopment } from "../../common/vars";
|
import { isDevelopment } from "../../common/vars";
|
||||||
import url from "url";
|
import url from "url";
|
||||||
import { makeObservable, observable } from "mobx";
|
import { makeObservable, observable } from "mobx";
|
||||||
import type { ParsedUrlQueryInput } from "querystring";
|
import { ipcRenderer } from "electron";
|
||||||
|
|
||||||
export enum TerminalChannels {
|
export enum TerminalChannels {
|
||||||
STDIN = 0,
|
STDIN = 0,
|
||||||
@ -50,7 +50,7 @@ enum TerminalColor {
|
|||||||
export type TerminalApiQuery = Record<string, string> & {
|
export type TerminalApiQuery = Record<string, string> & {
|
||||||
id: string;
|
id: string;
|
||||||
node?: string;
|
node?: string;
|
||||||
type?: string | "node";
|
type?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class TerminalApi extends WebSocketApi {
|
export class TerminalApi extends WebSocketApi {
|
||||||
@ -58,9 +58,8 @@ export class TerminalApi extends WebSocketApi {
|
|||||||
|
|
||||||
public onReady = new EventEmitter<[]>();
|
public onReady = new EventEmitter<[]>();
|
||||||
@observable public isReady = false;
|
@observable public isReady = false;
|
||||||
public readonly url: string;
|
|
||||||
|
|
||||||
constructor(protected options: TerminalApiQuery) {
|
constructor(protected query: TerminalApiQuery) {
|
||||||
super({
|
super({
|
||||||
logging: isDevelopment,
|
logging: isDevelopment,
|
||||||
flushOnOpen: false,
|
flushOnOpen: false,
|
||||||
@ -68,30 +67,41 @@ export class TerminalApi extends WebSocketApi {
|
|||||||
});
|
});
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
|
||||||
const { hostname, protocol, port } = location;
|
if (query.node) {
|
||||||
const query: ParsedUrlQueryInput = {
|
query.type ||= "node";
|
||||||
id: options.id,
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
if (options.node) {
|
async connect() {
|
||||||
query.node = options.node;
|
if (!this.socket) {
|
||||||
query.type = options.type || "node";
|
/**
|
||||||
|
* Only emit this message if we are not "reconnecting", so as to keep the
|
||||||
|
* output display clean when the computer wakes from sleep
|
||||||
|
*/
|
||||||
|
this.emitStatus("Connecting ...");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.url = url.format({
|
const authTokenArray = await ipcRenderer.invoke("cluster:shell-api", getHostedClusterId(), this.query.id);
|
||||||
|
|
||||||
|
if (!(authTokenArray instanceof Uint8Array)) {
|
||||||
|
throw new TypeError("ShellApi token is not a Uint8Array");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hostname, protocol, port } = location;
|
||||||
|
const socketUrl = url.format({
|
||||||
protocol: protocol.includes("https") ? "wss" : "ws",
|
protocol: protocol.includes("https") ? "wss" : "ws",
|
||||||
hostname,
|
hostname,
|
||||||
port,
|
port,
|
||||||
pathname: "/api",
|
pathname: "/api",
|
||||||
query,
|
query: {
|
||||||
|
...this.query,
|
||||||
|
shellToken: Buffer.from(authTokenArray).toString("base64"),
|
||||||
|
},
|
||||||
slashes: true,
|
slashes: true,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
this.emitStatus("Connecting ...");
|
|
||||||
this.onData.addListener(this._onReady, { prepend: true });
|
this.onData.addListener(this._onReady, { prepend: true });
|
||||||
super.connect(this.url);
|
super.connect(socketUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
|||||||
@ -37,11 +37,11 @@ interface IMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum WebSocketApiState {
|
export enum WebSocketApiState {
|
||||||
PENDING = -1,
|
PENDING = "pending",
|
||||||
OPEN,
|
OPEN = "open",
|
||||||
CONNECTING,
|
CONNECTING = "connecting",
|
||||||
RECONNECTING,
|
RECONNECTING = "reconnecting",
|
||||||
CLOSED,
|
CLOSED = "closed",
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebSocketApi {
|
export class WebSocketApi {
|
||||||
|
|||||||
@ -49,9 +49,9 @@ const LargeTooltip = withStyles({
|
|||||||
@observer
|
@observer
|
||||||
export class HelmChartDetails extends Component<Props> {
|
export class HelmChartDetails extends Component<Props> {
|
||||||
@observable chartVersions: HelmChart[];
|
@observable chartVersions: HelmChart[];
|
||||||
@observable selectedChart: HelmChart;
|
@observable selectedChart?: HelmChart;
|
||||||
@observable readme: string = null;
|
@observable readme?: string;
|
||||||
@observable error: string = null;
|
@observable error?: string;
|
||||||
|
|
||||||
private abortController?: AbortController;
|
private abortController?: AbortController;
|
||||||
|
|
||||||
@ -68,6 +68,10 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => this.props.chart, async ({ name, repo, version }) => {
|
reaction(() => this.props.chart, async ({ name, repo, version }) => {
|
||||||
try {
|
try {
|
||||||
|
this.selectedChart = undefined;
|
||||||
|
this.chartVersions = undefined;
|
||||||
|
this.readme = undefined;
|
||||||
|
|
||||||
const { readme, versions } = await getChartDetails(repo, name, { version });
|
const { readme, versions } = await getChartDetails(repo, name, { version });
|
||||||
|
|
||||||
this.readme = readme;
|
this.readme = readme;
|
||||||
|
|||||||
@ -26,12 +26,9 @@ import type { ItemObject } from "../../../common/item.store";
|
|||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { navigation } from "../../navigation";
|
import { navigation } from "../../navigation";
|
||||||
import { searchUrlParam } from "../input";
|
import { searchUrlParam } from "../input";
|
||||||
import { makeCss } from "../../../common/utils/makeCss";
|
|
||||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||||
import type { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import type { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
|
|
||||||
const css = makeCss(styles);
|
|
||||||
|
|
||||||
export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
|
export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
|
||||||
constructor(public entity: T, private registry: CatalogEntityRegistry) {
|
constructor(public entity: T, private registry: CatalogEntityRegistry) {
|
||||||
if (!(entity instanceof CatalogEntity)) {
|
if (!(entity instanceof CatalogEntity)) {
|
||||||
@ -79,7 +76,7 @@ export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
|
|||||||
return this.labels
|
return this.labels
|
||||||
.map(label => (
|
.map(label => (
|
||||||
<Badge
|
<Badge
|
||||||
className={css.badge}
|
className={styles.badge}
|
||||||
key={label}
|
key={label}
|
||||||
label={label}
|
label={label}
|
||||||
title={label}
|
title={label}
|
||||||
|
|||||||
@ -19,59 +19,77 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.list :global(.TableRow) {
|
.Catalog {
|
||||||
.entityName {
|
:global(.TableHead) .entityName {
|
||||||
position: relative;
|
/* offset for icon to align texts in the column */
|
||||||
width: min-content;
|
padding-left: calc(21px + var(--padding) * 2.5);
|
||||||
max-width: 100%;
|
}
|
||||||
|
|
||||||
|
:global(.TableRow):hover .pinIcon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.entityName {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 var(--padding);
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-right: 24px; // + reserved space for .pinIcon
|
||||||
|
|
||||||
|
> span {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
padding-right: 24px;
|
padding-left: var(--padding);
|
||||||
|
}
|
||||||
|
|
||||||
.pinIcon {
|
:global(.HotbarIcon){
|
||||||
position: absolute;
|
align-self: center;
|
||||||
right: 0;
|
|
||||||
opacity: 0;
|
|
||||||
transition: none;
|
|
||||||
|
|
||||||
&:hover {
|
div {
|
||||||
/* Drop styles defined for <Icon/> */
|
/* icons with plain text */
|
||||||
background-color: transparent;
|
font-size: var(--unit);
|
||||||
box-shadow: none;
|
}
|
||||||
}
|
|
||||||
|
.Icon {
|
||||||
|
/* icons with font-icon */
|
||||||
|
font-size: var(--small-size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .pinIcon {
|
.pinIcon {
|
||||||
opacity: 1;
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
transition: none;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
/* Drop styles defined for <Icon/> */
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconCell {
|
|
||||||
@apply flex items-center max-w-[40px];
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconCell > div * {
|
|
||||||
font-size: var(--unit);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nameCell {
|
|
||||||
}
|
|
||||||
|
|
||||||
.sourceCell {
|
.sourceCell {
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statusCell {
|
.statusCell {
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
}
|
|
||||||
|
|
||||||
.connected, .available {
|
:global {
|
||||||
color: var(--colorSuccess);
|
.connected, .available {
|
||||||
}
|
color: var(--colorSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
.disconnected, .deleting, .unavailable {
|
.disconnected, .deleting, .unavailable {
|
||||||
color: var(--halfGray);
|
color: var(--halfGray);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.labelsCell {
|
.labelsCell {
|
||||||
|
|||||||
@ -37,8 +37,7 @@ import { CatalogAddButton } from "./catalog-add-button";
|
|||||||
import type { RouteComponentProps } from "react-router";
|
import type { RouteComponentProps } from "react-router";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { MainLayout } from "../layout/main-layout";
|
import { MainLayout } from "../layout/main-layout";
|
||||||
import { createStorage, cssNames, prevDefault } from "../../utils";
|
import { createStorage, prevDefault } from "../../utils";
|
||||||
import { makeCss } from "../../../common/utils/makeCss";
|
|
||||||
import { CatalogEntityDetails } from "./catalog-entity-details";
|
import { CatalogEntityDetails } from "./catalog-entity-details";
|
||||||
import { browseCatalogTab, catalogURL, CatalogViewRouteParam } from "../../../common/routes";
|
import { browseCatalogTab, catalogURL, CatalogViewRouteParam } from "../../../common/routes";
|
||||||
import { CatalogMenu } from "./catalog-menu";
|
import { CatalogMenu } from "./catalog-menu";
|
||||||
@ -56,8 +55,6 @@ enum sortBy {
|
|||||||
status = "status",
|
status = "status",
|
||||||
}
|
}
|
||||||
|
|
||||||
const css = makeCss(styles);
|
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<CatalogViewRouteParam> {
|
interface Props extends RouteComponentProps<CatalogViewRouteParam> {
|
||||||
catalogEntityStore?: CatalogEntityStore;
|
catalogEntityStore?: CatalogEntityStore;
|
||||||
}
|
}
|
||||||
@ -73,6 +70,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
this.catalogEntityStore = props.catalogEntityStore;
|
this.catalogEntityStore = props.catalogEntityStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
catalogEntityStore: new CatalogEntityStore(),
|
catalogEntityStore: new CatalogEntityStore(),
|
||||||
};
|
};
|
||||||
@ -105,7 +103,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
this.activeTab = routeTab;
|
this.activeTab = routeTab;
|
||||||
this.catalogEntityStore.activeCategory = item;
|
this.catalogEntityStore.activeCategory = item;
|
||||||
});
|
});
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Notifications.error(<p>Unknown category: {routeTab}</p>);
|
Notifications.error(<p>Unknown category: {routeTab}</p>);
|
||||||
}
|
}
|
||||||
@ -177,7 +175,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
|
|
||||||
renderNavigation() {
|
renderNavigation() {
|
||||||
return (
|
return (
|
||||||
<CatalogMenu activeItem={this.activeTab} onItemClick={this.onTabChange}/>
|
<CatalogMenu activeItem={this.activeTab} onItemClick={this.onTabChange} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,23 +212,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
const isItemInHotbar = HotbarStore.getInstance().isAddedToActive(item.entity);
|
const isItemInHotbar = HotbarStore.getInstance().isAddedToActive(item.entity);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.entityName}>
|
<>
|
||||||
{item.name}
|
|
||||||
<Icon
|
|
||||||
small
|
|
||||||
className={styles.pinIcon}
|
|
||||||
material={!isItemInHotbar && "push_pin"}
|
|
||||||
svg={isItemInHotbar && "push_off"}
|
|
||||||
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
|
|
||||||
onClick={prevDefault(() => isItemInHotbar ? this.removeFromHotbar(item) : this.addToHotbar(item))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIcon(item: CatalogEntityItem<CatalogEntity>) {
|
|
||||||
return (
|
|
||||||
<RenderDelay>
|
|
||||||
<HotbarIcon
|
<HotbarIcon
|
||||||
uid={`catalog-icon-${item.getId()}`}
|
uid={`catalog-icon-${item.getId()}`}
|
||||||
title={item.getName()}
|
title={item.getName()}
|
||||||
@ -240,7 +222,16 @@ export class Catalog extends React.Component<Props> {
|
|||||||
background={item.entity.spec.icon?.background}
|
background={item.entity.spec.icon?.background}
|
||||||
size={24}
|
size={24}
|
||||||
/>
|
/>
|
||||||
</RenderDelay>
|
<span>{item.name}</span>
|
||||||
|
<Icon
|
||||||
|
small
|
||||||
|
className={styles.pinIcon}
|
||||||
|
material={!isItemInHotbar && "push_pin"}
|
||||||
|
svg={isItemInHotbar ? "push_off" : "push_pin"}
|
||||||
|
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
|
||||||
|
onClick={prevDefault(() => isItemInHotbar ? this.removeFromHotbar(item) : this.addToHotbar(item))}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,11 +245,11 @@ export class Catalog extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemListLayout
|
<ItemListLayout
|
||||||
|
className={styles.Catalog}
|
||||||
tableId={tableId}
|
tableId={tableId}
|
||||||
renderHeaderTitle={activeCategory?.metadata.name || "Browse All"}
|
renderHeaderTitle={activeCategory?.metadata.name || "Browse All"}
|
||||||
isSelectable={false}
|
isSelectable={false}
|
||||||
isConfigurable={true}
|
isConfigurable={true}
|
||||||
className={styles.list}
|
|
||||||
store={this.catalogEntityStore}
|
store={this.catalogEntityStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: item => item.name,
|
[sortBy.name]: item => item.name,
|
||||||
@ -270,23 +261,21 @@ export class Catalog extends React.Component<Props> {
|
|||||||
entity => entity.searchFields,
|
entity => entity.searchFields,
|
||||||
]}
|
]}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "", className: css.iconCell, id: "icon" },
|
{ title: "Name", className: styles.entityName, sortBy: sortBy.name, id: "name" },
|
||||||
{ title: "Name", className: css.nameCell, sortBy: sortBy.name, id: "name" },
|
!activeCategory && { title: "Kind", sortBy: sortBy.kind, id: "kind" },
|
||||||
!activeCategory && { title: "Kind", className: css.kindCell, sortBy: sortBy.kind, id: "kind" },
|
{ title: "Source", className: styles.sourceCell, sortBy: sortBy.source, id: "source" },
|
||||||
{ title: "Source", className: css.sourceCell, sortBy: sortBy.source, id: "source" },
|
{ title: "Labels", className: styles.labelsCell, id: "labels" },
|
||||||
{ title: "Labels", className: css.labelsCell, id: "labels" },
|
{ title: "Status", className: styles.statusCell, sortBy: sortBy.status, id: "status" },
|
||||||
{ title: "Status", className: css.statusCell, sortBy: sortBy.status, id: "status" },
|
|
||||||
].filter(Boolean)}
|
].filter(Boolean)}
|
||||||
customizeTableRowProps={item => ({
|
customizeTableRowProps={item => ({
|
||||||
disabled: !item.enabled,
|
disabled: !item.enabled,
|
||||||
})}
|
})}
|
||||||
renderTableContents={item => [
|
renderTableContents={item => [
|
||||||
this.renderIcon(item),
|
|
||||||
this.renderName(item),
|
this.renderName(item),
|
||||||
!activeCategory && item.kind,
|
!activeCategory && item.kind,
|
||||||
item.source,
|
item.source,
|
||||||
item.getLabelBadges(),
|
item.getLabelBadges(),
|
||||||
{ title: item.phase, className: cssNames(css[item.phase]) },
|
<span key="phase" className={item.phase}>{item.phase}</span>,
|
||||||
].filter(Boolean)}
|
].filter(Boolean)}
|
||||||
onDetails={this.onDetails}
|
onDetails={this.onDetails}
|
||||||
renderItemMenu={this.renderItemMenu}
|
renderItemMenu={this.renderItemMenu}
|
||||||
@ -302,7 +291,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<MainLayout sidebar={this.renderNavigation()}>
|
<MainLayout sidebar={this.renderNavigation()}>
|
||||||
<div className="p-6 h-full">
|
<div className="p-6 h-full">
|
||||||
{ this.renderList() }
|
{this.renderList()}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
this.catalogEntityStore.selectedItem
|
this.catalogEntityStore.selectedItem
|
||||||
|
|||||||
@ -50,11 +50,14 @@ interface Props extends RouteComponentProps<HpaRouteParams> {
|
|||||||
export class HorizontalPodAutoscalers extends React.Component<Props> {
|
export class HorizontalPodAutoscalers extends React.Component<Props> {
|
||||||
getTargets(hpa: HorizontalPodAutoscaler) {
|
getTargets(hpa: HorizontalPodAutoscaler) {
|
||||||
const metrics = hpa.getMetrics();
|
const metrics = hpa.getMetrics();
|
||||||
const metricsRemainCount = metrics.length - 1;
|
|
||||||
const metricsRemain = metrics.length > 1 ? <>{metricsRemainCount} more...</> : null;
|
|
||||||
const metricValues = hpa.getMetricValues(metrics[0]);
|
|
||||||
|
|
||||||
return <p>{metricValues} {metricsRemain && "+"}{metricsRemain}</p>;
|
if (metrics.length === 0) {
|
||||||
|
return <p>--</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metricsRemain = metrics.length > 1 ? `+${metrics.length - 1} more...` : "";
|
||||||
|
|
||||||
|
return <p>{hpa.getMetricValues(metrics[0])} {metricsRemain}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -28,7 +28,7 @@ import { observable, makeObservable, reaction } from "mobx";
|
|||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { addPortForward, getPortForward, openPortForward, PortForwardDialog, portForwardStore, predictProtocol, removePortForward } from "../../port-forward";
|
import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, openPortForward, PortForwardDialog, portForwardStore, predictProtocol, removePortForward } from "../../port-forward";
|
||||||
import type { ForwardedPort } from "../../port-forward";
|
import type { ForwardedPort } from "../../port-forward";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
|
|
||||||
@ -93,12 +93,20 @@ export class ServicePortComponent extends React.Component<Props> {
|
|||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// determine how many port-forwards are already active
|
||||||
|
const { length } = await getPortForwards();
|
||||||
|
|
||||||
this.forwardPort = await addPortForward(portForward);
|
this.forwardPort = await addPortForward(portForward);
|
||||||
|
|
||||||
if (this.forwardPort) {
|
if (this.forwardPort) {
|
||||||
portForward.forwardPort = this.forwardPort;
|
portForward.forwardPort = this.forwardPort;
|
||||||
openPortForward(portForward);
|
openPortForward(portForward);
|
||||||
this.isPortForwarded = true;
|
this.isPortForwarded = true;
|
||||||
|
|
||||||
|
// if this is the first port-forward show the about notification
|
||||||
|
if (!length) {
|
||||||
|
aboutPortForwarding();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
||||||
|
|||||||
@ -28,7 +28,7 @@ import { observable, makeObservable, reaction } from "mobx";
|
|||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { addPortForward, getPortForward, openPortForward, PortForwardDialog, portForwardStore, predictProtocol, removePortForward } from "../../port-forward";
|
import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, openPortForward, PortForwardDialog, portForwardStore, predictProtocol, removePortForward } from "../../port-forward";
|
||||||
import type { ForwardedPort } from "../../port-forward";
|
import type { ForwardedPort } from "../../port-forward";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
|
|
||||||
@ -97,12 +97,20 @@ export class PodContainerPort extends React.Component<Props> {
|
|||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// determine how many port-forwards are already active
|
||||||
|
const { length } = await getPortForwards();
|
||||||
|
|
||||||
this.forwardPort = await addPortForward(portForward);
|
this.forwardPort = await addPortForward(portForward);
|
||||||
|
|
||||||
if (this.forwardPort) {
|
if (this.forwardPort) {
|
||||||
portForward.forwardPort = this.forwardPort;
|
portForward.forwardPort = this.forwardPort;
|
||||||
openPortForward(portForward);
|
openPortForward(portForward);
|
||||||
this.isPortForwarded = true;
|
this.isPortForwarded = true;
|
||||||
|
|
||||||
|
// if this is the first port-forward show the about notification
|
||||||
|
if (!length) {
|
||||||
|
aboutPortForwarding();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
||||||
|
|||||||
@ -69,7 +69,7 @@ export class Pods extends React.Component<Props> {
|
|||||||
formatters: {
|
formatters: {
|
||||||
tableView: true,
|
tableView: true,
|
||||||
},
|
},
|
||||||
children: Object.keys(state).map(status => (
|
children: Object.keys(state).map((status: keyof typeof state) => (
|
||||||
<Fragment key={status}>
|
<Fragment key={status}>
|
||||||
<div className="title">
|
<div className="title">
|
||||||
{name} <span className="text-secondary">({status}{ready ? ", ready" : ""})</span>
|
{name} <span className="text-secondary">({status}{ready ? ", ready" : ""})</span>
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { computed, makeObservable, reaction } from "mobx";
|
|||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import type { RouteComponentProps } from "react-router";
|
import type { RouteComponentProps } from "react-router";
|
||||||
import { ClusterStatus } from "./cluster-status";
|
import { ClusterStatus } from "./cluster-status";
|
||||||
import { hasLoadedView, initView, refreshViews } from "./lens-views";
|
import { ClusterFrameHandler } from "./lens-views";
|
||||||
import type { Cluster } from "../../../main/cluster";
|
import type { Cluster } from "../../../main/cluster";
|
||||||
import { ClusterStore } from "../../../common/cluster-store";
|
import { ClusterStore } from "../../../common/cluster-store";
|
||||||
import { requestMain } from "../../../common/ipc";
|
import { requestMain } from "../../../common/ipc";
|
||||||
@ -57,7 +57,7 @@ export class ClusterView extends React.Component<Props> {
|
|||||||
@computed get isReady(): boolean {
|
@computed get isReady(): boolean {
|
||||||
const { cluster, clusterId } = this;
|
const { cluster, clusterId } = this;
|
||||||
|
|
||||||
return cluster?.ready && cluster?.available && hasLoadedView(clusterId);
|
return cluster?.ready && cluster?.available && ClusterFrameHandler.getInstance().hasLoadedView(clusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -65,25 +65,23 @@ export class ClusterView extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
refreshViews();
|
ClusterFrameHandler.getInstance().clearVisibleCluster();
|
||||||
catalogEntityRegistry.activeEntity = null;
|
catalogEntityRegistry.activeEntity = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => this.clusterId, async (clusterId) => {
|
reaction(() => this.clusterId, async (clusterId) => {
|
||||||
refreshViews(clusterId); // refresh visibility of active cluster
|
ClusterFrameHandler.getInstance().setVisibleCluster(clusterId);
|
||||||
initView(clusterId); // init cluster-view (iframe), requires parent container #lens-views to be in DOM
|
ClusterFrameHandler.getInstance().initView(clusterId);
|
||||||
requestMain(clusterActivateHandler, clusterId, false); // activate and fetch cluster's state from main
|
requestMain(clusterActivateHandler, clusterId, false); // activate and fetch cluster's state from main
|
||||||
catalogEntityRegistry.activeEntity = catalogEntityRegistry.getById(clusterId);
|
catalogEntityRegistry.activeEntity = clusterId;
|
||||||
}, {
|
}, {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
reaction(() => [this.cluster?.ready, this.cluster?.disconnected], (values) => {
|
reaction(() => [this.cluster?.ready, this.cluster?.disconnected], ([, disconnected]) => {
|
||||||
const disconnected = values[1];
|
if (ClusterFrameHandler.getInstance().hasLoadedView(this.clusterId) && disconnected) {
|
||||||
|
|
||||||
if (hasLoadedView(this.clusterId) && disconnected) {
|
|
||||||
navigate(catalogURL()); // redirect to catalog when active cluster get disconnected/not available
|
navigate(catalogURL()); // redirect to catalog when active cluster get disconnected/not available
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -19,82 +19,117 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { observable, when } from "mobx";
|
import { action, IReactionDisposer, makeObservable, observable, reaction, when } from "mobx";
|
||||||
import logger from "../../../main/logger";
|
import logger from "../../../main/logger";
|
||||||
import { requestMain } from "../../../common/ipc";
|
|
||||||
import { clusterVisibilityHandler } from "../../../common/cluster-ipc";
|
import { clusterVisibilityHandler } from "../../../common/cluster-ipc";
|
||||||
import { ClusterStore } from "../../../common/cluster-store";
|
import { ClusterStore } from "../../../common/cluster-store";
|
||||||
import type { ClusterId } from "../../../common/cluster-types";
|
import type { ClusterId } from "../../../common/cluster-types";
|
||||||
import { getClusterFrameUrl } from "../../utils";
|
import { getClusterFrameUrl, Singleton } from "../../utils";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
|
||||||
export interface LensView {
|
export interface LensView {
|
||||||
isLoaded?: boolean
|
isLoaded: boolean;
|
||||||
clusterId: ClusterId;
|
frame: HTMLIFrameElement;
|
||||||
view: HTMLIFrameElement
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const lensViews = observable.map<ClusterId, LensView>();
|
export class ClusterFrameHandler extends Singleton {
|
||||||
|
private views = observable.map<string, LensView>();
|
||||||
|
@observable private visibleCluster: string | null = null;
|
||||||
|
|
||||||
export function hasLoadedView(clusterId: ClusterId): boolean {
|
constructor() {
|
||||||
return !!lensViews.get(clusterId)?.isLoaded;
|
super();
|
||||||
}
|
makeObservable(this);
|
||||||
|
reaction(() => this.visibleCluster, this.handleVisibleClusterChange);
|
||||||
export async function initView(clusterId: ClusterId) {
|
|
||||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
|
||||||
|
|
||||||
if (!cluster || lensViews.has(clusterId)) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`);
|
public hasLoadedView(clusterId: string): boolean {
|
||||||
const parentElem = document.getElementById("lens-views");
|
return Boolean(this.views.get(clusterId)?.isLoaded);
|
||||||
const iframe = document.createElement("iframe");
|
|
||||||
|
|
||||||
iframe.id = `cluster-frame-${cluster.id}`;
|
|
||||||
iframe.name = cluster.contextName;
|
|
||||||
iframe.setAttribute("src", getClusterFrameUrl(clusterId));
|
|
||||||
iframe.addEventListener("load", () => {
|
|
||||||
logger.info(`[LENS-VIEW]: loaded from ${iframe.src}`);
|
|
||||||
lensViews.get(clusterId).isLoaded = true;
|
|
||||||
}, { once: true });
|
|
||||||
lensViews.set(clusterId, { clusterId, view: iframe });
|
|
||||||
parentElem.appendChild(iframe);
|
|
||||||
|
|
||||||
logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await when(() => cluster.ready, { timeout: 5_000 }); // we cannot wait forever because cleanup would be blocked for broken cluster connections
|
|
||||||
logger.info(`[LENS-VIEW]: cluster is ready, clusterId=${clusterId}`);
|
|
||||||
} finally {
|
|
||||||
await autoCleanOnRemove(clusterId, iframe);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) {
|
@action
|
||||||
await when(() => {
|
public initView(clusterId: ClusterId) {
|
||||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||||
|
|
||||||
return !cluster || (cluster.disconnected && lensViews.get(clusterId)?.isLoaded);
|
if (!cluster || this.views.has(clusterId)) {
|
||||||
});
|
return;
|
||||||
logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`);
|
}
|
||||||
lensViews.delete(clusterId);
|
|
||||||
|
|
||||||
iframe.parentNode.removeChild(iframe);
|
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`);
|
||||||
}
|
const parentElem = document.getElementById("lens-views");
|
||||||
|
const iframe = document.createElement("iframe");
|
||||||
export function refreshViews(visibleClusterId?: string) {
|
|
||||||
logger.info(`[LENS-VIEW]: refreshing iframe views, visible cluster id=${visibleClusterId}`);
|
iframe.id = `cluster-frame-${cluster.id}`;
|
||||||
const cluster = ClusterStore.getInstance().getById(visibleClusterId);
|
iframe.name = cluster.contextName;
|
||||||
|
iframe.style.display = "none";
|
||||||
lensViews.forEach(({ clusterId, view, isLoaded }) => {
|
iframe.setAttribute("src", getClusterFrameUrl(clusterId));
|
||||||
const isCurrent = clusterId === cluster?.id;
|
iframe.addEventListener("load", () => {
|
||||||
const isReady = cluster?.available && cluster?.ready;
|
logger.info(`[LENS-VIEW]: loaded from ${iframe.src}`);
|
||||||
const isVisible = isCurrent && isLoaded && isReady;
|
this.views.get(clusterId).isLoaded = true;
|
||||||
|
}, { once: true });
|
||||||
view.style.display = isVisible ? "flex" : "none";
|
this.views.set(clusterId, { frame: iframe, isLoaded: false });
|
||||||
|
parentElem.appendChild(iframe);
|
||||||
requestMain(clusterVisibilityHandler, clusterId, isVisible).catch(() => {
|
|
||||||
logger.error(`[LENS-VIEW]: failed to set cluster visibility, clusterId=${clusterId}`);
|
logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`);
|
||||||
});
|
|
||||||
});
|
const dispose = when(
|
||||||
|
() => cluster.ready,
|
||||||
|
() => logger.info(`[LENS-VIEW]: cluster is ready, clusterId=${clusterId}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
when(
|
||||||
|
// cluster.disconnect is set to `false` when the cluster starts to connect
|
||||||
|
() => !cluster.disconnected,
|
||||||
|
() => {
|
||||||
|
when(
|
||||||
|
() => {
|
||||||
|
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||||
|
|
||||||
|
return !cluster || (cluster.disconnected && this.views.get(clusterId)?.isLoaded);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`);
|
||||||
|
this.views.delete(clusterId);
|
||||||
|
iframe.parentNode.removeChild(iframe);
|
||||||
|
dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setVisibleCluster(clusterId: ClusterId) {
|
||||||
|
this.visibleCluster = clusterId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearVisibleCluster() {
|
||||||
|
this.visibleCluster = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private prevVisibleClusterChange?: IReactionDisposer;
|
||||||
|
|
||||||
|
private handleVisibleClusterChange = (clusterId: ClusterId | undefined) => {
|
||||||
|
logger.info(`[LENS-VIEW]: refreshing iframe views, visible cluster id=${clusterId}`);
|
||||||
|
|
||||||
|
ipcRenderer.send(clusterVisibilityHandler);
|
||||||
|
|
||||||
|
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||||
|
|
||||||
|
for (const { frame: view } of this.views.values()) {
|
||||||
|
view.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cluster) {
|
||||||
|
const lensView = this.views.get(clusterId);
|
||||||
|
|
||||||
|
this.prevVisibleClusterChange?.();
|
||||||
|
this.prevVisibleClusterChange = when(
|
||||||
|
() => cluster.available && cluster.ready && lensView.isLoaded,
|
||||||
|
() => {
|
||||||
|
logger.info(`[LENS-VIEW]: cluster id=${clusterId} should now be visible`);
|
||||||
|
lensView.frame.style.display = "flex";
|
||||||
|
ipcRenderer.send(clusterVisibilityHandler, clusterId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export class TerminalWindow extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activate(tabId: TabId) {
|
activate(tabId: TabId) {
|
||||||
if (this.terminal) this.terminal.detach(); // detach previous
|
this.terminal?.detach(); // detach previous
|
||||||
this.terminal = TerminalStore.getInstance().getTerminal(tabId);
|
this.terminal = TerminalStore.getInstance().getTerminal(tabId);
|
||||||
this.terminal.attachTo(this.elem);
|
this.terminal.attachTo(this.elem);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,16 +26,15 @@ import { FitAddon } from "xterm-addon-fit";
|
|||||||
import { dockStore, TabId } from "./dock.store";
|
import { dockStore, TabId } from "./dock.store";
|
||||||
import type { TerminalApi } from "../../api/terminal-api";
|
import type { TerminalApi } from "../../api/terminal-api";
|
||||||
import { ThemeStore } from "../../theme.store";
|
import { ThemeStore } from "../../theme.store";
|
||||||
import { boundMethod } from "../../utils";
|
import { boundMethod, disposer } from "../../utils";
|
||||||
import { isMac } from "../../../common/vars";
|
import { isMac } from "../../../common/vars";
|
||||||
import { camelCase } from "lodash";
|
import { camelCase } from "lodash";
|
||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import { clipboard } from "electron";
|
import { clipboard } from "electron";
|
||||||
|
import logger from "../../../common/logger";
|
||||||
|
|
||||||
export class Terminal {
|
export class Terminal {
|
||||||
static spawningPool: HTMLElement;
|
public static readonly spawningPool = (() => {
|
||||||
|
|
||||||
static init() {
|
|
||||||
// terminal element must be in DOM before attaching via xterm.open(elem)
|
// terminal element must be in DOM before attaching via xterm.open(elem)
|
||||||
// https://xtermjs.org/docs/api/terminal/classes/terminal/#open
|
// https://xtermjs.org/docs/api/terminal/classes/terminal/#open
|
||||||
const pool = document.createElement("div");
|
const pool = document.createElement("div");
|
||||||
@ -43,8 +42,9 @@ export class Terminal {
|
|||||||
pool.className = "terminal-init";
|
pool.className = "terminal-init";
|
||||||
pool.style.cssText = "position: absolute; top: 0; left: 0; height: 0; visibility: hidden; overflow: hidden";
|
pool.style.cssText = "position: absolute; top: 0; left: 0; height: 0; visibility: hidden; overflow: hidden";
|
||||||
document.body.appendChild(pool);
|
document.body.appendChild(pool);
|
||||||
Terminal.spawningPool = pool;
|
|
||||||
}
|
return pool;
|
||||||
|
})();
|
||||||
|
|
||||||
static async preloadFonts() {
|
static async preloadFonts() {
|
||||||
const fontPath = require("../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires
|
const fontPath = require("../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires
|
||||||
@ -54,13 +54,22 @@ export class Terminal {
|
|||||||
document.fonts.add(fontFace);
|
document.fonts.add(fontFace);
|
||||||
}
|
}
|
||||||
|
|
||||||
public xterm: XTerm;
|
private xterm: XTerm | null = new XTerm({
|
||||||
public fitAddon: FitAddon;
|
cursorBlink: true,
|
||||||
public scrollPos = 0;
|
cursorStyle: "bar",
|
||||||
public disposers: Function[] = [];
|
fontSize: 13,
|
||||||
|
fontFamily: "RobotoMono",
|
||||||
|
});
|
||||||
|
private readonly fitAddon = new FitAddon();
|
||||||
|
private scrollPos = 0;
|
||||||
|
private disposer = disposer();
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
protected setTheme(colors: Record<string, string>) {
|
protected setTheme(colors: Record<string, string>) {
|
||||||
|
if (!this.xterm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Replacing keys stored in styles to format accepted by terminal
|
// Replacing keys stored in styles to format accepted by terminal
|
||||||
// E.g. terminalBrightBlack -> brightBlack
|
// E.g. terminalBrightBlack -> brightBlack
|
||||||
const colorPrefix = "terminal";
|
const colorPrefix = "terminal";
|
||||||
@ -73,17 +82,13 @@ export class Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get elem() {
|
get elem() {
|
||||||
return this.xterm.element;
|
return this.xterm?.element;
|
||||||
}
|
}
|
||||||
|
|
||||||
get viewport() {
|
get viewport() {
|
||||||
return this.xterm.element.querySelector(".xterm-viewport");
|
return this.xterm.element.querySelector(".xterm-viewport");
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(public tabId: TabId, protected api: TerminalApi) {
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
get isActive() {
|
get isActive() {
|
||||||
const { isOpen, selectedTabId } = dockStore;
|
const { isOpen, selectedTabId } = dockStore;
|
||||||
|
|
||||||
@ -96,22 +101,15 @@ export class Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
detach() {
|
detach() {
|
||||||
Terminal.spawningPool.appendChild(this.elem);
|
const { elem } = this;
|
||||||
|
|
||||||
|
if (elem) {
|
||||||
|
Terminal.spawningPool.appendChild(elem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
constructor(public tabId: TabId, protected api: TerminalApi) {
|
||||||
if (this.xterm) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.xterm = new XTerm({
|
|
||||||
cursorBlink: true,
|
|
||||||
cursorStyle: "bar",
|
|
||||||
fontSize: 13,
|
|
||||||
fontFamily: "RobotoMono",
|
|
||||||
});
|
|
||||||
|
|
||||||
// enable terminal addons
|
// enable terminal addons
|
||||||
this.fitAddon = new FitAddon();
|
|
||||||
this.xterm.loadAddon(this.fitAddon);
|
this.xterm.loadAddon(this.fitAddon);
|
||||||
|
|
||||||
this.xterm.open(Terminal.spawningPool);
|
this.xterm.open(Terminal.spawningPool);
|
||||||
@ -128,7 +126,7 @@ export class Terminal {
|
|||||||
this.api.onData.addListener(this.onApiData);
|
this.api.onData.addListener(this.onApiData);
|
||||||
window.addEventListener("resize", this.onResize);
|
window.addEventListener("resize", this.onResize);
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposer.push(
|
||||||
reaction(() => ThemeStore.getInstance().activeTheme.colors, this.setTheme, {
|
reaction(() => ThemeStore.getInstance().activeTheme.colors, this.setTheme, {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
}),
|
}),
|
||||||
@ -142,16 +140,18 @@ export class Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (!this.xterm) return;
|
if (this.xterm) {
|
||||||
this.disposers.forEach(dispose => dispose());
|
this.disposer();
|
||||||
this.disposers = [];
|
this.xterm.dispose();
|
||||||
this.xterm.dispose();
|
this.xterm = null;
|
||||||
this.xterm = null;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fit = () => {
|
fit = () => {
|
||||||
// Since this function is debounced we need to read this value as late as possible
|
// Since this function is debounced we need to read this value as late as possible
|
||||||
if (!this.isActive) return;
|
if (!this.isActive || !this.xterm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.fitAddon.fit();
|
this.fitAddon.fit();
|
||||||
@ -159,9 +159,8 @@ export class Terminal {
|
|||||||
|
|
||||||
this.api.sendTerminalSize(cols, rows);
|
this.api.sendTerminalSize(cols, rows);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
// see https://github.com/lensapp/lens/issues/1891
|
||||||
|
logger.error(`[TERMINAL]: failed to resize terminal to fit`, error);
|
||||||
return; // see https://github.com/lensapp/lens/issues/1891
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -204,19 +203,21 @@ export class Terminal {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onContextMenu = () => {
|
onContextMenu = () => {
|
||||||
const { terminalCopyOnSelect } = UserStore.getInstance();
|
if (
|
||||||
const textFromClipboard = clipboard.readText();
|
// don't paste if user hasn't turned on the feature
|
||||||
|
UserStore.getInstance().terminalCopyOnSelect
|
||||||
|
|
||||||
if (terminalCopyOnSelect) {
|
// don't paste if the clipboard doesn't have text
|
||||||
this.xterm.paste(textFromClipboard);
|
&& clipboard.availableFormats().includes("text/plain")
|
||||||
|
) {
|
||||||
|
this.xterm.paste(clipboard.readText());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onSelectionChange = () => {
|
onSelectionChange = () => {
|
||||||
const { terminalCopyOnSelect } = UserStore.getInstance();
|
|
||||||
const selection = this.xterm.getSelection().trim();
|
const selection = this.xterm.getSelection().trim();
|
||||||
|
|
||||||
if (terminalCopyOnSelect && selection !== "") {
|
if (UserStore.getInstance().terminalCopyOnSelect && selection) {
|
||||||
clipboard.writeText(selection);
|
clipboard.writeText(selection);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -251,5 +252,3 @@ export class Terminal {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Terminal.init();
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 959 B After Width: | Height: | Size: 959 B |
@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* 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 "@testing-library/jest-dom/extend-expect";
|
||||||
|
import { render, fireEvent } from "@testing-library/react";
|
||||||
|
import { SidebarCluster } from "../sidebar-cluster";
|
||||||
|
import { KubernetesCluster } from "../../../../common/catalog-entities";
|
||||||
|
|
||||||
|
jest.mock("../../../../common/hotbar-store", () => ({
|
||||||
|
HotbarStore: {
|
||||||
|
getInstance: () => ({
|
||||||
|
isAddedToActive: jest.fn(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const clusterEntity = new KubernetesCluster({
|
||||||
|
metadata: {
|
||||||
|
uid: "test-uid",
|
||||||
|
name: "test-cluster",
|
||||||
|
source: "local",
|
||||||
|
labels: {},
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
kubeconfigPath: "",
|
||||||
|
kubeconfigContext: "",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "connected",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("<SidebarCluster/>", () => {
|
||||||
|
it("renders w/o errors", () => {
|
||||||
|
const { container } = render(<SidebarCluster clusterEntity={clusterEntity}/>);
|
||||||
|
|
||||||
|
expect(container).toBeInstanceOf(HTMLElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders cluster avatar and name", () => {
|
||||||
|
const { getByText } = render(<SidebarCluster clusterEntity={clusterEntity}/>);
|
||||||
|
|
||||||
|
expect(getByText("tc")).toBeInTheDocument();
|
||||||
|
expect(getByText("test-cluster")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders cluster menu", async () => {
|
||||||
|
const { getByTestId, getByText } = render(<SidebarCluster clusterEntity={clusterEntity}/>);
|
||||||
|
const link = getByTestId("sidebar-cluster-dropdown");
|
||||||
|
|
||||||
|
fireEvent.click(link);
|
||||||
|
expect(await getByText("Add to Hotbar")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@ -19,12 +19,39 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
.SidebarCluster {
|
||||||
* Function expands generic CSS Modules literal types and adds dictionary with arbitrary
|
display: flex;
|
||||||
* indexes.
|
align-items: center;
|
||||||
* @param styles Styles imported from CSS Module having only literal types
|
padding: 1.25rem;
|
||||||
* @returns Passed style list with expanded typescript types
|
cursor: pointer;
|
||||||
*/
|
|
||||||
export function makeCss<T>(styles: T) {
|
&:focus-visible {
|
||||||
return styles as typeof styles & { [key: string]: string };
|
background: var(--blue);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--sidebarLogoBackground);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clusterName {
|
||||||
|
font-weight: bold;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
color: var(--textColorAccent);
|
||||||
|
display: -webkit-box;
|
||||||
|
/* Simulate text-overflow:ellipsis styles but for multiple text lines */
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
width: 185px;
|
||||||
|
margin-top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 1.25rem;
|
||||||
|
}
|
||||||
140
src/renderer/components/layout/sidebar-cluster.tsx
Normal file
140
src/renderer/components/layout/sidebar-cluster.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* 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 styles from "./sidebar-cluster.module.css";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { HotbarStore } from "../../../common/hotbar-store";
|
||||||
|
import { broadcastMessage } from "../../../common/ipc";
|
||||||
|
import type { CatalogEntity, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
|
||||||
|
import { IpcRendererNavigationEvents } from "../../navigation/events";
|
||||||
|
import { Avatar } from "../avatar/avatar";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { navigate } from "../../navigation";
|
||||||
|
import { Menu, MenuItem } from "../menu";
|
||||||
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
|
|
||||||
|
const contextMenu: CatalogEntityContextMenuContext = observable({
|
||||||
|
menuItems: [],
|
||||||
|
navigate: (url: string, forceMainFrame = true) => {
|
||||||
|
if (forceMainFrame) {
|
||||||
|
broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, url);
|
||||||
|
} else {
|
||||||
|
navigate(url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
||||||
|
if (menuItem.confirm) {
|
||||||
|
ConfirmDialog.open({
|
||||||
|
okButtonProps: {
|
||||||
|
primary: false,
|
||||||
|
accent: true,
|
||||||
|
},
|
||||||
|
ok: () => {
|
||||||
|
menuItem.onClick();
|
||||||
|
},
|
||||||
|
message: menuItem.confirm.message,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
menuItem.onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SidebarCluster({ clusterEntity }: { clusterEntity: CatalogEntity }) {
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
|
||||||
|
if (!clusterEntity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMenuOpen = () => {
|
||||||
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
const isAddedToActive = HotbarStore.getInstance().isAddedToActive(clusterEntity);
|
||||||
|
const title = isAddedToActive
|
||||||
|
? "Remove from Hotbar"
|
||||||
|
: "Add to Hotbar";
|
||||||
|
const onClick = isAddedToActive
|
||||||
|
? () => hotbarStore.removeFromHotbar(metadata.uid)
|
||||||
|
: () => hotbarStore.addToHotbar(clusterEntity);
|
||||||
|
|
||||||
|
contextMenu.menuItems = [{ title, onClick }];
|
||||||
|
clusterEntity.onContextMenuOpen(contextMenu);
|
||||||
|
|
||||||
|
toggle();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (evt: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (evt.code == "Space") {
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
setOpened(!opened);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { metadata, spec } = clusterEntity;
|
||||||
|
const id = `cluster-${metadata.uid}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={id}
|
||||||
|
className={styles.SidebarCluster}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
role="menubar"
|
||||||
|
data-testid="sidebar-cluster-dropdown"
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
title={metadata.name}
|
||||||
|
colorHash={`${metadata.name}-${metadata.source}`}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
src={spec.icon?.src}
|
||||||
|
className={styles.avatar}
|
||||||
|
/>
|
||||||
|
<div className={styles.clusterName}>
|
||||||
|
{metadata.name}
|
||||||
|
</div>
|
||||||
|
<Icon material="arrow_drop_down"/>
|
||||||
|
<Menu
|
||||||
|
usePortal
|
||||||
|
htmlFor={id}
|
||||||
|
isOpen={opened}
|
||||||
|
open={onMenuOpen}
|
||||||
|
closeOnClickItem
|
||||||
|
closeOnClickOutside
|
||||||
|
close={toggle}
|
||||||
|
className={styles.menu}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
contextMenu.menuItems.map((menuItem) => (
|
||||||
|
<MenuItem key={menuItem.title} onClick={() => onMenuItemClick(menuItem)}>
|
||||||
|
{menuItem.title}
|
||||||
|
</MenuItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -40,17 +40,3 @@
|
|||||||
padding: 3px;
|
padding: 3px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cluster {
|
|
||||||
@apply flex items-center m-5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clusterName {
|
|
||||||
@apply font-bold overflow-hidden;
|
|
||||||
word-break: break-word;
|
|
||||||
color: var(--textColorAccent);
|
|
||||||
display: -webkit-box;
|
|
||||||
/* Simulate text-overflow:ellipsis styles but for multiple text lines */
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import { Storage } from "../+storage";
|
|||||||
import { Network } from "../+network";
|
import { Network } from "../+network";
|
||||||
import { crdStore } from "../+custom-resources/crd.store";
|
import { crdStore } from "../+custom-resources/crd.store";
|
||||||
import { CustomResources } from "../+custom-resources/custom-resources";
|
import { CustomResources } from "../+custom-resources/custom-resources";
|
||||||
import { isActiveRoute, navigate } from "../../navigation";
|
import { isActiveRoute } from "../../navigation";
|
||||||
import { isAllowedResource } from "../../../common/utils/allowed-resource";
|
import { isAllowedResource } from "../../../common/utils/allowed-resource";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { ClusterPageMenuRegistration, ClusterPageMenuRegistry, ClusterPageRegistry, getExtensionPageUrl } from "../../../extensions/registries";
|
import { ClusterPageMenuRegistration, ClusterPageMenuRegistry, ClusterPageRegistry, getExtensionPageUrl } from "../../../extensions/registries";
|
||||||
@ -41,12 +41,7 @@ import { Apps } from "../+apps";
|
|||||||
import * as routes from "../../../common/routes";
|
import * as routes from "../../../common/routes";
|
||||||
import { Config } from "../+config";
|
import { Config } from "../+config";
|
||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
import { SidebarCluster } from "./sidebar-cluster";
|
||||||
import { makeObservable, observable } from "mobx";
|
|
||||||
import type { CatalogEntityContextMenuContext } from "../../../common/catalog";
|
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
|
||||||
import { broadcastMessage } from "../../../common/ipc";
|
|
||||||
import { IpcRendererNavigationEvents } from "../../navigation/events";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -55,21 +50,6 @@ interface Props {
|
|||||||
@observer
|
@observer
|
||||||
export class Sidebar extends React.Component<Props> {
|
export class Sidebar extends React.Component<Props> {
|
||||||
static displayName = "Sidebar";
|
static displayName = "Sidebar";
|
||||||
@observable private contextMenu: CatalogEntityContextMenuContext = {
|
|
||||||
menuItems: [],
|
|
||||||
navigate: (url: string, forceMainFrame = true) => {
|
|
||||||
if (forceMainFrame) {
|
|
||||||
broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, url);
|
|
||||||
} else {
|
|
||||||
navigate(url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
makeObservable(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
crdStore.reloadAll();
|
crdStore.reloadAll();
|
||||||
@ -198,44 +178,6 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCluster() {
|
|
||||||
if (!this.clusterEntity) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { metadata, spec } = this.clusterEntity;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.cluster}>
|
|
||||||
<HotbarIcon
|
|
||||||
uid={metadata.uid}
|
|
||||||
title={metadata.name}
|
|
||||||
source={metadata.source}
|
|
||||||
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}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get clusterEntity() {
|
get clusterEntity() {
|
||||||
return catalogEntityRegistry.activeEntity;
|
return catalogEntityRegistry.activeEntity;
|
||||||
}
|
}
|
||||||
@ -245,7 +187,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("flex flex-col", className)} data-testid="cluster-sidebar">
|
<div className={cssNames("flex flex-col", className)} data-testid="cluster-sidebar">
|
||||||
{this.renderCluster()}
|
<SidebarCluster clusterEntity={this.clusterEntity}/>
|
||||||
<div className={styles.sidebarNav}>
|
<div className={styles.sidebarNav}>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
id="cluster"
|
id="cluster"
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import { IpcRendererNavigationEvents } from "./navigation/events";
|
|||||||
import { catalogEntityRegistry } from "./api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "./api/catalog-entity-registry";
|
||||||
import logger from "../common/logger";
|
import logger from "../common/logger";
|
||||||
import { unmountComponentAtNode } from "react-dom";
|
import { unmountComponentAtNode } from "react-dom";
|
||||||
|
import { ClusterFrameHandler } from "./components/cluster-manager/lens-views";
|
||||||
|
|
||||||
injectSystemCAs();
|
injectSystemCAs();
|
||||||
|
|
||||||
@ -61,6 +62,12 @@ export class LensApp extends React.Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(props: {}) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
ClusterFrameHandler.createInstance();
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ipcRenderer.send(IpcRendererNavigationEvents.LOADED);
|
ipcRenderer.send(IpcRendererNavigationEvents.LOADED);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,4 +22,5 @@
|
|||||||
export * from "./port-forward.store";
|
export * from "./port-forward.store";
|
||||||
export * from "./port-forward-item";
|
export * from "./port-forward-item";
|
||||||
export * from "./port-forward-dialog";
|
export * from "./port-forward-dialog";
|
||||||
|
export * from "./port-forward-notify";
|
||||||
export * from "./port-forward-utils";
|
export * from "./port-forward-utils";
|
||||||
|
|||||||
@ -29,9 +29,9 @@ import { Wizard, WizardStep } from "../components/wizard";
|
|||||||
import { Input } from "../components/input";
|
import { Input } from "../components/input";
|
||||||
import { Notifications } from "../components/notifications";
|
import { Notifications } from "../components/notifications";
|
||||||
import { cssNames } from "../utils";
|
import { cssNames } from "../utils";
|
||||||
import { addPortForward, modifyPortForward } from "./port-forward.store";
|
import { addPortForward, getPortForwards, modifyPortForward } from "./port-forward.store";
|
||||||
import type { ForwardedPort } from "./port-forward-item";
|
import type { ForwardedPort } from "./port-forward-item";
|
||||||
import { openPortForward } from ".";
|
import { aboutPortForwarding, openPortForward } from ".";
|
||||||
import { Checkbox } from "../components/checkbox";
|
import { Checkbox } from "../components/checkbox";
|
||||||
|
|
||||||
interface Props extends Partial<DialogProps> {
|
interface Props extends Partial<DialogProps> {
|
||||||
@ -96,6 +96,9 @@ export class PortForwardDialog extends Component<Props> {
|
|||||||
const { currentPort, desiredPort, close } = this;
|
const { currentPort, desiredPort, close } = this;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// determine how many port-forwards are already active
|
||||||
|
const { length } = await getPortForwards();
|
||||||
|
|
||||||
let port: number;
|
let port: number;
|
||||||
|
|
||||||
portForward.protocol = dialogState.useHttps ? "https" : "http";
|
portForward.protocol = dialogState.useHttps ? "https" : "http";
|
||||||
@ -105,6 +108,11 @@ export class PortForwardDialog extends Component<Props> {
|
|||||||
} else {
|
} else {
|
||||||
portForward.forwardPort = desiredPort;
|
portForward.forwardPort = desiredPort;
|
||||||
port = await addPortForward(portForward);
|
port = await addPortForward(portForward);
|
||||||
|
|
||||||
|
// if this is the first port-forward show the about notification
|
||||||
|
if (!length) {
|
||||||
|
aboutPortForwarding();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialogState.openInBrowser) {
|
if (dialogState.openInBrowser) {
|
||||||
|
|||||||
58
src/renderer/port-forward/port-forward-notify.tsx
Normal file
58
src/renderer/port-forward/port-forward-notify.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* 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 { portForwardsURL } from "../../common/routes/port-forwards";
|
||||||
|
import { Button } from "../components/button";
|
||||||
|
import { Notifications, notificationsStore } from "../components/notifications";
|
||||||
|
import { navigate } from "../navigation";
|
||||||
|
import { getHostedClusterId } from "../utils";
|
||||||
|
|
||||||
|
|
||||||
|
export function aboutPortForwarding() {
|
||||||
|
const notificationId = `port-forward-notification-${getHostedClusterId()}`;
|
||||||
|
|
||||||
|
Notifications.info(
|
||||||
|
(
|
||||||
|
<div className="flex column gaps">
|
||||||
|
<b>Port Forwarding</b>
|
||||||
|
<p>
|
||||||
|
You can manage your port forwards on the Port Forwarding Page.
|
||||||
|
</p>
|
||||||
|
<div className="flex gaps row align-left box grow">
|
||||||
|
<Button
|
||||||
|
active
|
||||||
|
outlined
|
||||||
|
label="Go to Port Forwarding"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(portForwardsURL());
|
||||||
|
notificationsStore.remove(notificationId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: notificationId,
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -47,7 +47,7 @@ export class PortForwardStore extends ItemStore<PortForwardItem> {
|
|||||||
const savedPortForwards = this.storage.get(); // undefined on first load
|
const savedPortForwards = this.storage.get(); // undefined on first load
|
||||||
|
|
||||||
if (Array.isArray(savedPortForwards)) {
|
if (Array.isArray(savedPortForwards)) {
|
||||||
logger.info("[PORT_FORWARD] starting saved port-forwards");
|
logger.info("[PORT-FORWARD-STORE] starting saved port-forwards");
|
||||||
await Promise.all(savedPortForwards.map(addPortForward));
|
await Promise.all(savedPortForwards.map(addPortForward));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,11 +108,12 @@ export async function addPortForward(portForward: ForwardedPort): Promise<number
|
|||||||
|
|
||||||
response = await apiBase.post<PortForwardResult>(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}?port=${portForward.port}&forwardPort=${portForward.forwardPort}&protocol=${protocol}`);
|
response = await apiBase.post<PortForwardResult>(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}?port=${portForward.port}&forwardPort=${portForward.forwardPort}&protocol=${protocol}`);
|
||||||
|
|
||||||
if (response?.port && response.port != +portForward.forwardPort) {
|
// expecting the received port to be the specified port, unless the specified port is 0, which indicates any available port is suitable
|
||||||
logger.warn(`specified ${portForward.forwardPort} got ${response.port}`);
|
if (portForward.forwardPort && response?.port && response.port != +portForward.forwardPort) {
|
||||||
|
logger.warn(`[PORT-FORWARD-STORE] specified ${portForward.forwardPort} got ${response.port}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn("Error adding port-forward:", error, portForward);
|
logger.warn("[PORT-FORWARD-STORE] Error adding port-forward:", error, portForward);
|
||||||
throw(error);
|
throw(error);
|
||||||
}
|
}
|
||||||
portForwardStore.reset();
|
portForwardStore.reset();
|
||||||
@ -134,7 +135,7 @@ export async function getPortForward(portForward: ForwardedPort): Promise<number
|
|||||||
try {
|
try {
|
||||||
response = await apiBase.get<PortForwardResult>(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}?port=${portForward.port}&forwardPort=${portForward.forwardPort}${getProtocolQuery(portForward.protocol)}`);
|
response = await apiBase.get<PortForwardResult>(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}?port=${portForward.port}&forwardPort=${portForward.forwardPort}${getProtocolQuery(portForward.protocol)}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn("Error getting port-forward:", error, portForward);
|
logger.warn("[PORT-FORWARD-STORE] Error getting port-forward:", error, portForward);
|
||||||
throw(error);
|
throw(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +160,7 @@ export async function removePortForward(portForward: ForwardedPort) {
|
|||||||
await apiBase.del(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}?port=${portForward.port}&forwardPort=${portForward.forwardPort}`);
|
await apiBase.del(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}?port=${portForward.port}&forwardPort=${portForward.forwardPort}`);
|
||||||
await waitUntilFree(+portForward.forwardPort, 200, 1000);
|
await waitUntilFree(+portForward.forwardPort, 200, 1000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn("Error removing port-forward:", error, portForward);
|
logger.warn("[PORT-FORWARD-STORE] Error removing port-forward:", error, portForward);
|
||||||
throw(error);
|
throw(error);
|
||||||
}
|
}
|
||||||
portForwardStore.reset();
|
portForwardStore.reset();
|
||||||
@ -171,7 +172,7 @@ export async function getPortForwards(): Promise<ForwardedPort[]> {
|
|||||||
|
|
||||||
return response.portForwards;
|
return response.portForwards;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn("Error getting all port-forwards:", error);
|
logger.warn("[PORT-FORWARD-STORE] Error getting all port-forwards:", error);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
67
yarn.lock
67
yarn.lock
@ -1051,7 +1051,7 @@
|
|||||||
"@sentry/utils" "6.7.1"
|
"@sentry/utils" "6.7.1"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/types@6.13.3", "@sentry/types@^6.13.3":
|
"@sentry/types@6.13.3":
|
||||||
version "6.13.3"
|
version "6.13.3"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.13.3.tgz#63ad5b6735b0dfd90b3a256a9f8e77b93f0f66b2"
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.13.3.tgz#63ad5b6735b0dfd90b3a256a9f8e77b93f0f66b2"
|
||||||
integrity sha512-Vrz5CdhaTRSvCQjSyIFIaV9PodjAVFkzJkTRxyY7P77RcegMsRSsG1yzlvCtA99zG9+e6MfoJOgbOCwuZids5A==
|
integrity sha512-Vrz5CdhaTRSvCQjSyIFIaV9PodjAVFkzJkTRxyY7P77RcegMsRSsG1yzlvCtA99zG9+e6MfoJOgbOCwuZids5A==
|
||||||
@ -1061,6 +1061,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.7.1.tgz#c8263e1886df5e815570c4668eb40a1cfaa1c88b"
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.7.1.tgz#c8263e1886df5e815570c4668eb40a1cfaa1c88b"
|
||||||
integrity sha512-9AO7HKoip2MBMNQJEd6+AKtjj2+q9Ze4ooWUdEvdOVSt5drg7BGpK221/p9JEOyJAZwEPEXdcMd3VAIMiOb4MA==
|
integrity sha512-9AO7HKoip2MBMNQJEd6+AKtjj2+q9Ze4ooWUdEvdOVSt5drg7BGpK221/p9JEOyJAZwEPEXdcMd3VAIMiOb4MA==
|
||||||
|
|
||||||
|
"@sentry/types@^6.14.1":
|
||||||
|
version "6.14.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.14.1.tgz#0d562a7aa91253b7843723344b4ba03a010e6376"
|
||||||
|
integrity sha512-RIk3ZwQKZnASrYWfV5i4wbzVveHz8xLFAS2ySIMqh+hICKnB0N4/r8a1Of/84j7pj+iAbf5vPS85639eIf+9qg==
|
||||||
|
|
||||||
"@sentry/utils@6.13.3":
|
"@sentry/utils@6.13.3":
|
||||||
version "6.13.3"
|
version "6.13.3"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.13.3.tgz#188754d40afe693c3fcae410f9322531588a9926"
|
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.13.3.tgz#188754d40afe693c3fcae410f9322531588a9926"
|
||||||
@ -1601,10 +1606,10 @@
|
|||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
form-data "^3.0.0"
|
form-data "^3.0.0"
|
||||||
|
|
||||||
"@types/node@*", "@types/node@14.17.27", "@types/node@^14.6.2":
|
"@types/node@*", "@types/node@14.17.33", "@types/node@^14.6.2":
|
||||||
version "14.17.27"
|
version "14.17.33"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.27.tgz#5054610d37bb5f6e21342d0e6d24c494231f3b85"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.33.tgz#011ee28e38dc7aee1be032ceadf6332a0ab15b12"
|
||||||
integrity sha512-94+Ahf9IcaDuJTle/2b+wzvjmutxXAEXU6O81JHblYXUg2BDG+dnBy7VxIPHKAyEEDHzCMQydTJuWvrE+Aanzw==
|
integrity sha512-noEeJ06zbn3lOh4gqe2v7NMGS33jrulfNqYFDjjEbhpDEHR5VTxgYNQSBqBlJIsBJW3uEYDgD6kvMnrrhGzq8g==
|
||||||
|
|
||||||
"@types/node@^10.12.0":
|
"@types/node@^10.12.0":
|
||||||
version "10.17.24"
|
version "10.17.24"
|
||||||
@ -1766,10 +1771,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@^17.0.33":
|
"@types/react@*", "@types/react@^17.0.34":
|
||||||
version "17.0.33"
|
version "17.0.34"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.33.tgz#e01ae3de7613dac1094569880bb3792732203ad5"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.34.tgz#797b66d359b692e3f19991b6b07e4b0c706c0102"
|
||||||
integrity sha512-pLWntxXpDPaU+RTAuSGWGSEL2FRTNyRQOjSWDke/rxRg14ncsZvx8AKWMWZqvc1UOaJIAoObdZhAWvRaHFi5rw==
|
integrity sha512-46FEGrMjc2+8XhHXILr+3+/sTe3OfzSPU9YGKILLrUYbQ1CLQC9Daqo1KzENGXAWwrFwiY0l4ZbF20gRvgpWTg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/prop-types" "*"
|
"@types/prop-types" "*"
|
||||||
"@types/scheduler" "*"
|
"@types/scheduler" "*"
|
||||||
@ -3783,23 +3788,7 @@ color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4:
|
|||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||||
|
|
||||||
color-string@^1.5.2:
|
color-string@^1.5.2, color-string@^1.5.4, color-string@^1.6.0:
|
||||||
version "1.5.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
|
|
||||||
integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==
|
|
||||||
dependencies:
|
|
||||||
color-name "^1.0.0"
|
|
||||||
simple-swizzle "^0.2.2"
|
|
||||||
|
|
||||||
color-string@^1.5.4:
|
|
||||||
version "1.5.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014"
|
|
||||||
integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==
|
|
||||||
dependencies:
|
|
||||||
color-name "^1.0.0"
|
|
||||||
simple-swizzle "^0.2.2"
|
|
||||||
|
|
||||||
color-string@^1.6.0:
|
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312"
|
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312"
|
||||||
integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==
|
integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==
|
||||||
@ -4092,11 +4081,16 @@ core-js@^3.6.5:
|
|||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
|
||||||
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
|
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
|
||||||
|
|
||||||
core-util-is@1.0.2, core-util-is@~1.0.0:
|
core-util-is@1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||||
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
|
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
|
||||||
|
|
||||||
|
core-util-is@~1.0.0:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||||
|
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||||
|
|
||||||
cosmiconfig@^6.0.0:
|
cosmiconfig@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982"
|
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982"
|
||||||
@ -8617,9 +8611,9 @@ jss@10.6.0, jss@^10.5.1:
|
|||||||
object.assign "^4.1.1"
|
object.assign "^4.1.1"
|
||||||
|
|
||||||
jszip@^3.1.0:
|
jszip@^3.1.0:
|
||||||
version "3.5.0"
|
version "3.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6"
|
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9"
|
||||||
integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==
|
integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==
|
||||||
dependencies:
|
dependencies:
|
||||||
lie "~3.3.0"
|
lie "~3.3.0"
|
||||||
pako "~1.0.2"
|
pako "~1.0.2"
|
||||||
@ -9767,10 +9761,9 @@ node-fetch@2.6.1:
|
|||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||||
|
|
||||||
node-fetch@^2.6.6:
|
node-fetch@lensapp/node-fetch#2.x:
|
||||||
version "2.6.6"
|
version "2.6.6"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
|
resolved "https://codeload.github.com/lensapp/node-fetch/tar.gz/c869d40ba7dd3bce392c34e36118c225b6ada8fb"
|
||||||
integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url "^5.0.0"
|
whatwg-url "^5.0.0"
|
||||||
|
|
||||||
@ -13218,10 +13211,10 @@ table@^6.0.9:
|
|||||||
string-width "^4.2.3"
|
string-width "^4.2.3"
|
||||||
strip-ansi "^6.0.1"
|
strip-ansi "^6.0.1"
|
||||||
|
|
||||||
tailwindcss@^2.2.17:
|
tailwindcss@^2.2.19:
|
||||||
version "2.2.17"
|
version "2.2.19"
|
||||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-2.2.17.tgz#c6332731f9ff1b6628ff589c95c38685347775e3"
|
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-2.2.19.tgz#540e464832cd462bb9649c1484b0a38315c2653c"
|
||||||
integrity sha512-WgRpn+Pxn7eWqlruxnxEbL9ByVRWi3iC10z4b6dW0zSdnkPVC4hPMSWLQkkW8GCyBIv/vbJ0bxIi9dVrl4CfoA==
|
integrity sha512-6Ui7JSVtXadtTUo2NtkBBacobzWiQYVjYW0ZnKaP9S1ZCKQ0w7KVNz+YSDI/j7O7KCMHbOkz94ZMQhbT9pOqjw==
|
||||||
dependencies:
|
dependencies:
|
||||||
arg "^5.0.1"
|
arg "^5.0.1"
|
||||||
bytes "^3.0.0"
|
bytes "^3.0.0"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user