+
{context}
{isNew && }
@@ -102,7 +104,7 @@ export class AddCluster extends React.Component {
httpsProxy: proxyServer || undefined,
},
});
- navigation.goBack(); // return to previous opened page for the cluster view
+ navigate(clusterViewURL({ params: { clusterId } }))
} catch (err) {
this.error = String(err);
} finally {
@@ -124,7 +126,7 @@ export class AddCluster extends React.Component {
to allow you to operate easily on multiple clusters and/or contexts.
- For more information on kubeconfig see Kubernetes docs
+ For more information on kubeconfig see Kubernetes docs .
NOTE: Any manually added cluster is not merged into your kubeconfig file.
@@ -137,22 +139,20 @@ export class AddCluster extends React.Component {
app.
- OIDC (OpenID Connect)
+ OIDC (OpenID Connect)
-
-
- When connecting Lens to OIDC enabled cluster, there's few things you as a user need to take into account.
-
-
Dedicated refresh token
-
- As Lens app utilized kubeconfig is "disconnected" from your main kubeconfig Lens needs to have it's own refresh token it utilizes.
- If you share the refresh token with e.g. kubectl who ever uses the token first will invalidate it for the next user.
- One way to achieve this is with kubelogin tool by removing the tokens
- (both id_token and refresh_token) from
- the config and issuing kubelogin command. That'll take you through the login process and will result you having "dedicated" refresh token.
-
-
-
Exec auth plugins
+
+ When connecting Lens to OIDC enabled cluster, there's few things you as a user need to take into account.
+
+
Dedicated refresh token
+
+ As Lens app utilized kubeconfig is "disconnected" from your main kubeconfig Lens needs to have it's own refresh token it utilizes.
+ If you share the refresh token with e.g. kubectl who ever uses the token first will invalidate it for the next user.
+ One way to achieve this is with kubelogin tool by removing the tokens
+ (both id_token and refresh_token) from
+ the config and issuing kubelogin command. That'll take you through the login process and will result you having "dedicated" refresh token.
+
+
Exec auth plugins
When using exec auth plugins make sure the paths that are used to call
any binaries
@@ -167,12 +167,14 @@ export class AddCluster extends React.Component {
return (
Add Cluster
+ Choose config:
Select kubeconfig}
value={this.clusterConfig}
options={this.clusterOptions}
onChange={({ value }: SelectOption) => this.clusterConfig = value}
formatOptionLabel={this.formatClusterContextLabel}
+ id="kubecontext-select"
/>
{this.showSettings && (
+
HTTP Proxy server. Used for communicating with Kubernetes API.
:
)`)}
value={this.proxyServer}
onChange={value => this.proxyServer = value}
+ theme="round-black"
/>
- HTTP Proxy server. Used for communicating with Kubernetes API.
+ {'A HTTP proxy server URL (format: http://:).'}
)}
@@ -197,6 +200,7 @@ export class AddCluster extends React.Component {
Kubeconfig:
this.customConfig = value}
@@ -209,7 +213,7 @@ export class AddCluster extends React.Component {
Add cluster}
+ label={Add cluster(s) }
onClick={this.addCluster}
waiting={this.isWaiting}
/>
diff --git a/src/renderer/components/+cluster-settings/cluster-settings.route.ts b/src/renderer/components/+cluster-settings/cluster-settings.route.ts
index 7501ba60bd..a2c7a45fd8 100644
--- a/src/renderer/components/+cluster-settings/cluster-settings.route.ts
+++ b/src/renderer/components/+cluster-settings/cluster-settings.route.ts
@@ -1,8 +1,12 @@
+import type { IClusterViewRouteParams } from "../cluster-manager/cluster-view.route";
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
-export const clusterSettingsRoute: RouteProps = {
- path: "/cluster-settings"
+export interface IClusterSettingsRouteParams extends IClusterViewRouteParams {
}
-export const clusterSettingsURL = buildURL(clusterSettingsRoute.path)
+export const clusterSettingsRoute: RouteProps = {
+ path: `/cluster/:clusterId/settings`,
+}
+
+export const clusterSettingsURL = buildURL(clusterSettingsRoute.path)
diff --git a/src/renderer/components/+cluster-settings/cluster-settings.scss b/src/renderer/components/+cluster-settings/cluster-settings.scss
index 1ea6926eab..f20699de35 100644
--- a/src/renderer/components/+cluster-settings/cluster-settings.scss
+++ b/src/renderer/components/+cluster-settings/cluster-settings.scss
@@ -1,86 +1,83 @@
.ClusterSettings {
- overflow-y: scroll;
+ .WizardLayout {
grid-template-columns: unset;
+ grid-template-rows: 76px 1fr;
+ padding: 0;
- .info-col {
- display: none;
+ .head-col {
+ justify-content: space-between;
+
+ :nth-child(2) {
+ flex: 1 0 0;
+ }
}
.content-col {
- margin-right: unset;
- }
+ margin: 0;
+ padding-top: $padding * 3;
+ background-color: transparent;
- * {
- margin-top: 40px;
+ .SubTitle {
+ text-transform: none;
+ }
- &:first-child {
- margin-top: 0px;
- }
- }
+ > div {
+ margin-top: $margin * 5;
+ }
- h4 {
- margin-top: 20px;
+ .admin-note {
+ font-size: small;
+ opacity: 0.5;
+ margin-left: $margin;
+ }
+
+ .button-area {
+ margin-top: $margin * 2;
+ }
+
+ .file-loader {
+ margin-top: $margin * 2;
+ }
+
+ .hint {
+ font-size: smaller;
+ opacity: 0.8;
+ }
+
+ p + p, .hint + p {
+ padding-top: $padding;
+ }
}
.status-table {
- margin-top: 20px;
- display: grid;
- grid-template-columns: 1fr 3fr;
- grid-gap: 10px;
- }
+ margin: $margin * 3 0;
- .loading {
- margin-top: 20px;
- text-align: center;
+ .Table {
+ border: 1px solid var(--drawerSubtitleBackground);
+ border-radius: $radius;
- .Spinner {
- display: inline-block;
+ .TableRow {
+ &:not(:last-of-type) {
+ border-bottom: 1px solid var(--drawerSubtitleBackground);
+ }
+
+ .value {
+ flex-grow: 2;
+ word-break: break-word;
+ color: var(--textColorSecondary);
+ }
}
+ }
}
- .Input,.Select {
- margin-top: 10px;
+ .Input, .Select {
+ margin-top: 10px;
}
- .Icon:not(.updated):not(.clean) {
- color: #ad0000;
- }
-
- .Icon.updated {
- color: #00dd1d;
- }
-
- .updated {
- animation: updated-name 1s 1;
- animation-fill-mode: forwards;
- animation-delay: 3s;
- }
-
- @keyframes updated-name {
- from {opacity :1;}
- to {opacity :0;}
- }
-
- .center {
- text-align: center;
- }
-
- input[type="text"]::placeholder {
- font-size: small;
- color: #707070;
- }
-
- input[type="text"] {
- color: white;
- }
-
- button {
- margin-top: 5px;
-
- .Spinner {
- width: 10px;
- height: 10px;
- border-color: transparent black;
- }
+ .Select {
+ &__control {
+ box-shadow: 0 0 0 1px $borderFaintColor;
+ }
}
+ }
}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/cluster-settings.tsx b/src/renderer/components/+cluster-settings/cluster-settings.tsx
index 60c5925513..7f1f0382fc 100644
--- a/src/renderer/components/+cluster-settings/cluster-settings.tsx
+++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx
@@ -1,24 +1,43 @@
-import "./cluster-settings.scss"
+import "./cluster-settings.scss";
+
import React from "react";
+import { Link } from "react-router-dom";
import { observer } from "mobx-react";
-import { Features } from "./features"
-import { Removal } from "./removal"
-import { Status } from "./status"
-import { General } from "./general"
-import { getHostedCluster } from "../../../common/cluster-store"
+import { Features } from "./features";
+import { Removal } from "./removal";
+import { Status } from "./status";
+import { General } from "./general";
import { WizardLayout } from "../layout/wizard-layout";
+import { ClusterIcon } from "../cluster-icon";
+import { Icon } from "../icon";
+import { getMatchedCluster } from "../cluster-manager/cluster-view.route";
+import { navigate } from "../../navigation";
@observer
export class ClusterSettings extends React.Component {
render() {
- const cluster = getHostedCluster();
+ const cluster = getMatchedCluster();
+ if (!cluster) return null;
+ const header = (
+ <>
+
+ {cluster.preferences.clusterName}
+ navigate("/")} big/>
+ >
+ );
return (
-
-
-
-
-
-
- )
+
+
+
+
+
+
+
+
+ );
}
}
diff --git a/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx
index c8779c692a..f998035b44 100644
--- a/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx
+++ b/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx
@@ -1,86 +1,43 @@
import React from "react";
-import { Cluster } from "../../../../main/cluster";
-import { Input } from "../../input";
-import { Spinner } from "../../spinner";
-import { clusterStore } from "../../../../common/cluster-store"
-import { Icon } from "../../icon";
-import { Tooltip, TooltipPosition } from "../../tooltip";
-import { autobind } from "../../../utils";
-import { TextInputStatus } from "./statuses"
import { observable } from "mobx";
import { observer } from "mobx-react";
+import { Cluster } from "../../../../main/cluster";
+import { Input } from "../../input";
+import { SubTitle } from "../../layout/sub-title";
interface Props {
- cluster: Cluster;
+ cluster: Cluster;
}
@observer
export class ClusterHomeDirSetting extends React.Component {
@observable directory = this.props.cluster.preferences.terminalCWD || "";
- @observable status = TextInputStatus.CLEAN;
- @observable errorText?: string;
+
+ save = () => {
+ this.props.cluster.preferences.terminalCWD = this.directory;
+ };
+
+ onChange = (value: string) => {
+ this.directory = value;
+ }
render() {
- return <>
- Working Directory
- Set initial working directory for terminals. When set it will the `pwd` when a new terminal instance is opened for this cluster.
-
- >;
- }
-
- @autobind()
- onWorkingDirectoryChange(directory: string, _e: React.ChangeEvent) {
- if (this.status === TextInputStatus.UPDATING) {
- console.log("prevent changing cluster directory while updating");
- return;
- }
-
- this.status = this.dirDiffers(directory);
- this.directory = directory;
- }
-
- dirDiffers(directory: string): TextInputStatus {
- const { terminalCWD = "" } = this.props.cluster.preferences;
-
- return directory === terminalCWD ? TextInputStatus.CLEAN : TextInputStatus.DIRTY;
- }
-
- getIconRight(): React.ReactNode {
- switch (this.status) {
- case TextInputStatus.CLEAN:
- return null;
- case TextInputStatus.DIRTY:
- return ;
- case TextInputStatus.UPDATED:
- return ;
- case TextInputStatus.UPDATING:
- return ;
- case TextInputStatus.ERROR:
- return
-
- {this.errorText}
-
-
- }
- }
-
- @autobind()
- onWorkingDirectorySubmit(directory: string) {
- if (this.dirDiffers(directory) !== TextInputStatus.DIRTY) {
- return;
- }
-
- this.status = TextInputStatus.UPDATING
- this.props.cluster.preferences.terminalCWD = directory;
- this.directory = directory;
- this.status = TextInputStatus.UPDATED
+ return (
+ <>
+
+ Terminal working directory.
+
+
+ An explicit start path where the terminal will be launched,{" "}
+ this is used as the current working directory (cwd) for the shell process.
+
+ >
+ );
}
}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx
index 2a5f0d0b97..20b4e6d6d5 100644
--- a/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx
+++ b/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx
@@ -1,16 +1,20 @@
import React from "react";
import { Cluster } from "../../../../main/cluster";
-import { clusterStore } from "../../../../common/cluster-store"
-import { Icon } from "../../icon";
import { FilePicker, OverSizeLimitStyle } from "../../file-picker";
import { autobind } from "../../../utils";
import { Button } from "../../button";
-import { GeneralInputStatus } from "./statuses"
import { observable } from "mobx";
import { observer } from "mobx-react";
+import { SubTitle } from "../../layout/sub-title";
+import { ClusterIcon } from "../../cluster-icon";
+
+enum GeneralInputStatus {
+ CLEAN = "clean",
+ ERROR = "error",
+}
interface Props {
- cluster: Cluster;
+ cluster: Cluster;
}
@observer
@@ -21,7 +25,6 @@ export class ClusterIconSetting extends React.Component {
@autobind()
async onIconPick([file]: File[]) {
const { cluster } = this.props;
-
try {
if (file) {
const buf = Buffer.from(await file.arrayBuffer());
@@ -38,35 +41,36 @@ export class ClusterIconSetting extends React.Component {
}
getClearButton() {
- const { cluster } = this.props;
-
- if (cluster.preferences.icon) {
- return this.onIconPick([])}>Clear
+ if (this.props.cluster.preferences.icon) {
+ return this.onIconPick([])}>Clear
}
}
render() {
- return <>
- Cluster Icon
- Set cluster icon. By default it is automatically generated. {this.getIconRight()}
-
-
+
- {this.getClearButton()}
-
- >;
- }
-
- getIconRight(): React.ReactNode {
- switch (this.status) {
- case GeneralInputStatus.CLEAN:
- return null;
- case GeneralInputStatus.ERROR:
- return
- }
+ {"Browse for new icon..."}
+ >
+ );
+ return (
+ <>
+
+ Define cluster icon. By default automatically generated.
+
+
+ {this.getClearButton()}
+
+ >
+ );
}
}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx
index e605864030..8e2f8a2afa 100644
--- a/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx
+++ b/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx
@@ -1,85 +1,40 @@
import React from "react";
import { Cluster } from "../../../../main/cluster";
import { Input } from "../../input";
-import { Spinner } from "../../spinner";
-import { clusterStore } from "../../../../common/cluster-store"
-import { Icon } from "../../icon";
-import { Tooltip, TooltipPosition } from "../../tooltip";
-import { autobind } from "../../../utils";
-import { TextInputStatus } from "./statuses"
import { observable } from "mobx";
import { observer } from "mobx-react";
+import { SubTitle } from "../../layout/sub-title";
+import { isRequired } from "../../input/input.validators";
interface Props {
- cluster: Cluster;
+ cluster: Cluster;
}
@observer
export class ClusterNameSetting extends React.Component {
@observable name = this.props.cluster.preferences.clusterName || "";
- @observable status = TextInputStatus.CLEAN;
- @observable errorText?: string;
+
+ save = () => {
+ this.props.cluster.preferences.clusterName = this.name;
+ };
+
+ onChange = (value: string) => {
+ this.name = value;
+ }
render() {
- return <>
- Cluster Name
- Change cluster name:
-
- >;
- }
-
- @autobind()
- onClusterNameChange(name: string, _e: React.ChangeEvent) {
- if (this.status === TextInputStatus.UPDATING) {
- console.log("prevent changing cluster name while updating");
- return;
- }
-
- this.status = this.nameDiffers(name)
- this.name = name;
- }
-
- nameDiffers(name: string): TextInputStatus {
- const { clusterName } = this.props.cluster.preferences;
-
- return name === clusterName ? TextInputStatus.CLEAN : TextInputStatus.DIRTY;
- }
-
- getIconRight(): React.ReactNode {
- switch (this.status) {
- case TextInputStatus.CLEAN:
- return null;
- case TextInputStatus.DIRTY:
- return ;
- case TextInputStatus.UPDATED:
- return ;
- case TextInputStatus.UPDATING:
- return ;
- case TextInputStatus.ERROR:
- return
-
- {this.errorText}
-
-
- }
- }
-
- @autobind()
- onClusterNameSubmit(name: string) {
- if (this.nameDiffers(name) !== TextInputStatus.DIRTY) {
- return;
- }
-
- this.status = TextInputStatus.UPDATING
- this.props.cluster.preferences.clusterName = name;
- this.name = name;
- this.status = TextInputStatus.UPDATED
+ return (
+ <>
+
+ Define cluster name.
+
+ >
+ );
}
}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx
index 7596cc245b..729090629d 100644
--- a/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx
+++ b/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx
@@ -1,41 +1,105 @@
import React from "react";
-import { Cluster } from "../../../../main/cluster";
-import { clusterStore } from "../../../../common/cluster-store"
-import { Select, SelectOption, SelectProps } from "../../select";
-import { prometheusProviders } from "../../../../common/prometheus-providers";
-import { autobind } from "../../../utils";
-import { observable } from "mobx";
import { observer } from "mobx-react";
+import { prometheusProviders } from "../../../../common/prometheus-providers";
+import { Cluster } from "../../../../main/cluster";
+import { SubTitle } from "../../layout/sub-title";
+import { Select, SelectOption } from "../../select";
+import { Input } from "../../input";
+import { observable, computed } from "mobx";
-const prometheusGuide = "https://github.com/lensapp/lens/blob/master/troubleshooting/custom-prometheus.md";
const options: SelectOption[] = [
- { value: "", label: "Auto detect" },
+ { value: "", label: "Auto detect" },
...prometheusProviders.map(pp => ({value: pp.id, label: pp.name}))
];
interface Props {
- cluster: Cluster;
+ cluster: Cluster;
}
@observer
export class ClusterPrometheusSetting extends React.Component {
- @observable prometheusProvider = this.props.cluster.preferences.prometheusProvider?.type || "";
-
- render() {
- return <>
- Cluster Prometheus
- Use pre-installed Prometheus service for metrics. Please refer to this guide for possible configuration changes.
-
- >;
+ @observable path = "";
+ @observable provider = "";
+
+ @computed get canEditPrometheusPath() {
+ if (this.provider === "" || this.provider === "lens") return false;
+ return true;
}
- @autobind()
- changePrometheusProvider({ value: prometheusProvider }: SelectProps) {
- this.prometheusProvider = prometheusProvider;
- this.props.cluster.preferences.prometheusProvider = { type: prometheusProvider };
+ componentDidMount() {
+ const { prometheus, prometheusProvider } = this.props.cluster.preferences;
+ if (prometheus) {
+ const prefix = prometheus.prefix || "";
+ this.path = `${prometheus.namespace}/${prometheus.service}:${prometheus.port}${prefix}`;
+ }
+ if (prometheusProvider) {
+ this.provider = prometheusProvider.type;
+ }
+ }
+
+ parsePrometheusPath = () => {
+ if (!this.provider || !this.path) {
+ return null;
+ }
+ const parsed = this.path.split(/\/|:/, 3);
+ const apiPrefix = this.path.substring(parsed.join("/").length);
+ if (!parsed[0] || !parsed[1] || !parsed[2]) {
+ return null;
+ }
+ return {
+ namespace: parsed[0],
+ service: parsed[1],
+ port: parseInt(parsed[2]),
+ prefix: apiPrefix
+ }
+ }
+
+ onSaveProvider = () => {
+ this.props.cluster.preferences.prometheusProvider = this.provider ?
+ { type: this.provider } :
+ null;
+ }
+
+ onSavePath = () => {
+ this.props.cluster.preferences.prometheus = this.parsePrometheusPath();
+ };
+
+ render() {
+ return (
+ <>
+
+
+ Use pre-installed Prometheus service for metrics. Please refer to the{" "}
+ guide {" "}
+ for possible configuration changes.
+
+ Prometheus installation method.
+ {
+ this.provider = value;
+ this.onSaveProvider();
+ }}
+ options={options}
+ />
+ What query format is used to fetch metrics from Prometheus
+ {this.canEditPrometheusPath && (
+ <>
+ Prometheus service address.
+ this.path = value}
+ onBlur={this.onSavePath}
+ placeholder="/:"
+ />
+
+ An address to an existing Prometheus installation{" "}
+ ({'/:'}). Lens tries to auto-detect address if left empty.
+
+ >
+ )}
+ >
+ );
}
}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx
index 3ddcf25c7f..1b94992e5b 100644
--- a/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx
+++ b/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx
@@ -1,105 +1,41 @@
import React from "react";
-import { Cluster } from "../../../../main/cluster";
-import { Input } from "../../input";
-import { Spinner } from "../../spinner";
-import { clusterStore } from "../../../../common/cluster-store"
-import { Icon } from "../../icon";
-import { Tooltip, TooltipPosition } from "../../tooltip";
-import { autobind } from "../../../utils";
-import { TextInputStatus } from "./statuses"
import { observable } from "mobx";
import { observer } from "mobx-react";
+import { Cluster } from "../../../../main/cluster";
+import { Input } from "../../input";
+import { isUrl } from "../../input/input.validators";
+import { SubTitle } from "../../layout/sub-title";
interface Props {
- cluster: Cluster;
+ cluster: Cluster;
}
@observer
export class ClusterProxySetting extends React.Component {
@observable proxy = this.props.cluster.preferences.httpsProxy || "";
- @observable status = TextInputStatus.CLEAN;
- @observable errorText?: string;
+
+ save = () => {
+ this.props.cluster.preferences.httpsProxy = this.proxy;
+ };
+
+ onChange = (value: string) => {
+ this.proxy = value;
+ }
render() {
- return <>
- HTTPS Proxy
- HTTPS Proxy server. Used for communicating with Kubernetes API.
-
- >;
- }
-
- @autobind()
- changeProxyState(proxy: string, _e: React.ChangeEvent) {
- if (this.status === TextInputStatus.UPDATING) {
- console.log("prevent changing cluster proxy while updating");
- return;
- }
-
- this.status = this.proxyDiffers(proxy);
- this.proxy = proxy;
- }
-
- proxyDiffers(proxy: string): TextInputStatus {
- const { httpsProxy = "" } = this.props.cluster.preferences;
-
- return proxy === httpsProxy ? TextInputStatus.CLEAN : TextInputStatus.DIRTY;
- }
-
- getIconRight(): React.ReactNode {
- switch (this.status) {
- case TextInputStatus.CLEAN:
- return null;
- case TextInputStatus.DIRTY:
- return ;
- case TextInputStatus.UPDATED:
- return ;
- case TextInputStatus.UPDATING:
- return ;
- case TextInputStatus.ERROR:
- return
-
- {this.errorText}
-
-
- }
- }
-
- @autobind()
- updateClusterProxy(proxy: string) {
- if (this.proxyDiffers(proxy) !== TextInputStatus.DIRTY) {
- return;
- }
-
- try {
- const url = new URL(proxy);
-
- if (url.protocol !== "https") {
- this.status = TextInputStatus.ERROR
- this.errorText= `Proxy's protocol should be "https"`
- return
- }
- if (url.port === "") {
- this.status = TextInputStatus.ERROR
- this.errorText= "Proxy should include a port"
- return
- }
- } catch (e) {
- this.status = TextInputStatus.ERROR
- this.errorText= "Invalid URL"
- return
- }
-
- this.status = TextInputStatus.UPDATING
- this.props.cluster.preferences.httpsProxy = proxy;
- this.proxy = proxy;
- this.status = TextInputStatus.UPDATED
+ return (
+ <>
+
+ HTTP Proxy server. Used for communicating with Kubernetes API.
+
+ >
+ );
}
}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx
index 6337f56aa0..6cd933ca11 100644
--- a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx
+++ b/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx
@@ -1,36 +1,36 @@
import React from "react";
-import { Cluster } from "../../../../main/cluster";
-import { clusterStore } from "../../../../common/cluster-store"
-import { workspaceStore } from "../../../../common/workspace-store"
-import { Select, SelectOption } from "../../../components/select";
-import { GeneralInputStatus } from "./statuses"
-import { observable } from "mobx";
-import { autobind } from "../../../utils";
import { observer } from "mobx-react";
+import { Link } from "react-router-dom";
+import { workspacesURL } from "../../+workspaces";
+import { workspaceStore } from "../../../../common/workspace-store";
+import { Cluster } from "../../../../main/cluster";
+import { Select } from "../../../components/select";
+import { SubTitle } from "../../layout/sub-title";
interface Props {
- cluster: Cluster;
+ cluster: Cluster;
}
@observer
export class ClusterWorkspaceSetting extends React.Component {
- @observable workspace = this.props.cluster.workspace;
-
render() {
- return <>
- Cluster Workspace
- Change cluster workspace:
- ({value: w.id, label: {w.name} }))}
- onChange={this.changeWorkspace}
- />
- >;
- }
-
- @autobind()
- changeWorkspace({ value: workspace }: SelectOption) {
- this.workspace = workspace;
- this.props.cluster.workspace = workspace;
+ return (
+ <>
+
+
+ Define cluster{" "}
+
+ workspace
+ .
+
+ this.props.cluster.workspace = value}
+ options={workspaceStore.workspacesList.map(w =>
+ ({value: w.id, label: w.name})
+ )}
+ />
+ >
+ );
}
}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/components/install-feature.tsx b/src/renderer/components/+cluster-settings/components/install-feature.tsx
new file mode 100644
index 0000000000..a04b8fed63
--- /dev/null
+++ b/src/renderer/components/+cluster-settings/components/install-feature.tsx
@@ -0,0 +1,93 @@
+import React from "react";
+import { observable, reaction, comparer } from "mobx";
+import { observer, disposeOnUnmount } from "mobx-react";
+import { clusterIpc } from "../../../../common/cluster-ipc";
+import { Cluster } from "../../../../main/cluster";
+import { Button } from "../../button";
+import { Notifications } from "../../notifications";
+import { Spinner } from "../../spinner";
+
+interface Props {
+ cluster: Cluster
+ feature: string
+}
+
+@observer
+export class InstallFeature extends React.Component {
+ @observable loading = false;
+
+ componentDidMount() {
+ disposeOnUnmount(this,
+ reaction(() => this.props.cluster.features[this.props.feature], () => {
+ this.loading = false;
+ }, { equals: comparer.structural })
+ );
+ }
+
+ getActionButtons() {
+ const { cluster, feature } = this.props;
+ const features = cluster.features[feature];
+ const disabled = !cluster.isAdmin || this.loading;
+ const loadingIcon = this.loading ? : null;
+ if (!features) return null;
+ return (
+
+ {features.canUpgrade &&
+
+ clusterIpc.upgradeFeature.invokeFromRenderer(cluster.id, feature))
+ }
+ >
+ Upgrade
+
+ }
+ {features.installed &&
+
+ clusterIpc.uninstallFeature.invokeFromRenderer(cluster.id, feature))
+ }
+ >
+ Uninstall
+
+ }
+ {!features.installed && !features.canUpgrade &&
+
+ clusterIpc.installFeature.invokeFromRenderer(cluster.id, feature))
+ }
+ >
+ Install
+
+ }
+ {loadingIcon}
+ {!cluster.isAdmin && Actions can only be performed by admins. }
+
+ );
+ }
+
+ runAction(action: () => Promise): () => Promise {
+ return async () => {
+ try {
+ this.loading = true;
+ await action();
+ } catch (err) {
+ Notifications.error(err.toString());
+ }
+ };
+ }
+
+ render() {
+ return (
+ <>
+ {this.props.children}
+ {this.getActionButtons()}
+ >
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/components/install-metrics.tsx b/src/renderer/components/+cluster-settings/components/install-metrics.tsx
deleted file mode 100644
index 171adc6034..0000000000
--- a/src/renderer/components/+cluster-settings/components/install-metrics.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import React from "react";
-import { Cluster } from "../../../../main/cluster";
-import { Button } from "../../button";
-import { autobind } from "../../../utils";
-import { Tooltip, TooltipPosition } from "../../tooltip";
-import { MetricsFeature } from "../../../../features/metrics";
-import { Spinner } from "../../spinner";
-import { Icon } from "../../icon";
-import { clusterIpc } from "../../../../common/cluster-ipc";
-import { observable } from "mobx";
-import { ActionStatus } from "./statuses"
-import { observer } from "mobx-react";
-
-interface Props {
- cluster: Cluster;
-}
-
-@observer
-export class InstallMetrics extends React.Component {
- @observable status = ActionStatus.IDLE;
- @observable errorText?: string;
-
- render() {
- return <>
- Metrics
-
- User Mode feature enables non-admin users to see namespaces they have access to.
- This is achieved by configuring RBAC rules so that every authenticated user is granted to list namespaces.
-
-
- {this.getActionButtons()}
-
- >;
- }
-
- getStatusIcon(): React.ReactNode {
- switch (this.status) {
- case ActionStatus.IDLE:
- return null;
- case ActionStatus.PROCESSING:
- return ;
- case ActionStatus.ERROR:
- return
- }
- }
-
- getDisabledToolTip(id: string, action: string): React.ReactNode {
- const { cluster } = this.props;
- if (cluster.isAdmin) {
- return null;
- }
-
- return (
-
- {action} only allowed by admins
-
- );
- }
-
- getActionButtons(): React.ReactNode[] {
- const { cluster } = this.props
- const buttons = [];
-
- if (cluster.features[MetricsFeature.id]?.canUpgrade) {
- buttons.push(
-
- Upgrade {this.getStatusIcon()} {this.getDisabledToolTip("cluster-feature-metrics-upgrade", "Upgrading")}
-
- );
- }
-
- if (cluster.features[MetricsFeature.id]?.installed) {
- buttons.push(
-
- Uninstall {this.getStatusIcon()} {this.getDisabledToolTip("cluster-feature-metrics-uninstall", "Uninstalling")}
-
- );
- } else {
- buttons.push(
-
- Install {this.getStatusIcon()} {this.getDisabledToolTip("cluster-feature-metrics-install", "Installing")}
-
- );
- }
-
- return buttons;
- }
-
- runAction(action: keyof typeof clusterIpc): () => Promise {
- return async () => {
- const { cluster } = this.props;
- console.log(`running ${action} ${MetricsFeature.id} onto ${cluster.preferences.clusterName}`);
-
- try {
- this.status = ActionStatus.PROCESSING
- await clusterIpc[action].invokeFromRenderer(cluster.id, MetricsFeature.id);
- try {
- await cluster.refresh();
- } catch (err) {
- console.error(err);
- }
- this.status = ActionStatus.IDLE
- } catch (err) {
- this.status = ActionStatus.ERROR
- this.errorText = err.toString()
- }
- };
- }
-}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/components/install-user-mode.tsx b/src/renderer/components/+cluster-settings/components/install-user-mode.tsx
deleted file mode 100644
index faf7562336..0000000000
--- a/src/renderer/components/+cluster-settings/components/install-user-mode.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import React from "react";
-import { Cluster } from "../../../../main/cluster";
-import { Button } from "../../button";
-import { autobind } from "../../../utils";
-import { Tooltip, TooltipPosition } from "../../tooltip";
-import { Spinner } from "../../spinner";
-import { Icon } from "../../icon";
-import { UserModeFeature } from "../../../../features/user-mode";
-import { clusterIpc } from "../../../../common/cluster-ipc";
-import { observable } from "mobx";
-import { ActionStatus } from "./statuses"
-import { observer } from "mobx-react";
-
-interface Props {
- cluster: Cluster;
-}
-
-@observer
-export class InstallUserMode extends React.Component {
- @observable status = ActionStatus.IDLE;
- @observable errorText?: string;
-
- render() {
- return <>
- User Mode
-
- User Mode feature enables non-admin users to see namespaces they have access to.
- This is achieved by configuring RBAC rules so that every authenticated user is granted to list namespaces.
-
-
- {this.getActionButtons()}
-
- >;
- }
-
-
- getStatusIcon(): React.ReactNode {
- switch (this.status) {
- case ActionStatus.IDLE:
- return null;
- case ActionStatus.PROCESSING:
- return ;
- case ActionStatus.ERROR:
- return
- }
- }
-
- getDisabledToolTip(id: string, action: string): React.ReactNode {
- const { cluster } = this.props;
- if (cluster.isAdmin) {
- return null;
- }
-
- return
- {action} only allowed by admins
- ;
- }
-
- getActionButtons(): React.ReactNode[] {
- const { cluster } = this.props
- const buttons = [];
-
- if (cluster.features[UserModeFeature.id]?.canUpgrade) {
- buttons.push(
-
- Upgrade {this.getStatusIcon()} {this.getDisabledToolTip("cluster-feature-user-mode-upgrade", "Upgrading")}
-
- );
- }
-
- if (cluster.features[UserModeFeature.id]?.installed) {
- buttons.push(
-
- Uninstall {this.getStatusIcon()} {this.getDisabledToolTip("cluster-feature-user-mode-uninstall", "Uninstalling")}
-
- );
- } else {
- buttons.push(
-
- Install {this.getStatusIcon()} {this.getDisabledToolTip("cluster-feature-user-mode-install", "Installing")}
-
- );
- }
-
- return buttons;
- }
-
- runAction(action: keyof typeof clusterIpc): () => Promise {
- return async () => {
- const { cluster } = this.props;
- console.log(`running ${action} ${UserModeFeature.id} onto ${cluster.preferences.clusterName}`);
-
- try {
- this.status = ActionStatus.PROCESSING
- await clusterIpc[action].invokeFromRenderer(cluster.id, UserModeFeature.id);
- try {
- await cluster.refresh();
- } catch (err) {
- console.error(err);
- }
- this.status = ActionStatus.IDLE
- } catch (err) {
- this.status = ActionStatus.ERROR
- this.errorText = err.toString()
- }
- };
- }
-}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/components/remove-cluster-button.tsx b/src/renderer/components/+cluster-settings/components/remove-cluster-button.tsx
index 0c57196330..fe62ef4899 100644
--- a/src/renderer/components/+cluster-settings/components/remove-cluster-button.tsx
+++ b/src/renderer/components/+cluster-settings/components/remove-cluster-button.tsx
@@ -1,63 +1,37 @@
import React from "react";
-import { Cluster } from "../../../../main/cluster";
-import { Button } from "../../button";
-import { autobind } from "../../../utils";
-import { Spinner } from "../../spinner";
-import { Icon } from "../../icon";
-import { ConfirmDialog } from "../../confirm-dialog";
import { Trans } from "@lingui/macro";
+import { observer } from "mobx-react";
import { clusterIpc } from "../../../../common/cluster-ipc";
import { clusterStore } from "../../../../common/cluster-store";
-import { observable } from "mobx";
-import { observer } from "mobx-react";
-import { RemovalStatus } from "./statuses"
+import { Cluster } from "../../../../main/cluster";
+import { autobind } from "../../../utils";
+import { Button } from "../../button";
+import { ConfirmDialog } from "../../confirm-dialog";
interface Props {
- cluster: Cluster;
+ cluster: Cluster;
}
@observer
export class RemoveClusterButton extends React.Component {
- @observable status = RemovalStatus.PRESENT;
- @observable errorText?: string;
-
- render() {
- return (
-
- Remove Cluster {this.getStatusIcon()}
-
- );
- }
-
- getStatusIcon(): React.ReactNode {
- switch (this.status) {
- case RemovalStatus.PRESENT:
- return null;
- case RemovalStatus.PROCESSING:
- return ;
- case RemovalStatus.ERROR:
- return ;
- }
- }
-
- @autobind()
+ @autobind()
confirmRemoveCluster() {
const { cluster } = this.props;
-
ConfirmDialog.open({
message: Are you sure you want to remove {cluster.preferences.clusterName} from Lens?
,
labelOk: Yes ,
labelCancel: No ,
ok: async () => {
- try {
- this.status = RemovalStatus.PROCESSING;
- await clusterIpc.disconnect.invokeFromRenderer(cluster.id);
- await clusterStore.removeById(cluster.id);
- } catch (err) {
- this.status = RemovalStatus.ERROR;
- this.errorText = err.toString();
- }
+ await clusterStore.removeById(cluster.id);
}
})
}
+
+ render() {
+ return (
+
+ Remove Cluster
+
+ );
+ }
}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/components/statuses.ts b/src/renderer/components/+cluster-settings/components/statuses.ts
deleted file mode 100644
index d9d897c430..0000000000
--- a/src/renderer/components/+cluster-settings/components/statuses.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-export enum TextInputStatus {
- CLEAN = "clean",
- DIRTY = "dirty",
- UPDATING = "updating",
- ERROR = "error",
- UPDATED = "updated",
-}
-
-export enum GeneralInputStatus {
- CLEAN = "clean",
- ERROR = "error",
-}
-
-export enum ActionStatus {
- IDLE = "idle",
- PROCESSING = "processing",
- ERROR = "error"
-}
-
-export enum RemovalStatus {
- PRESENT = "present",
- PROCESSING = "processing",
- ERROR = "error",
-}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/features.tsx b/src/renderer/components/+cluster-settings/features.tsx
index b049fa90ff..f1b3cdada8 100644
--- a/src/renderer/components/+cluster-settings/features.tsx
+++ b/src/renderer/components/+cluster-settings/features.tsx
@@ -1,7 +1,9 @@
import React from "react";
import { Cluster } from "../../../main/cluster";
-import { InstallMetrics } from "./components/install-metrics";
-import { InstallUserMode } from "./components/install-user-mode";
+import { InstallFeature } from "./components/install-feature";
+import { SubTitle } from "../layout/sub-title";
+import { MetricsFeature } from "../../../features/metrics";
+import { UserModeFeature } from "../../../features/user-mode";
interface Props {
cluster: Cluster;
@@ -11,10 +13,30 @@ export class Features extends React.Component {
render() {
const { cluster } = this.props;
- return
-
Features
-
-
- ;
+ return (
+
+
Features
+
+ <>
+
+
+ Enable timeseries data visualization (Prometheus stack) for your cluster.
+ Install this only if you don't have existing Prometheus stack installed.
+ You can see preview of manifests{" "}
+ here .
+
+ >
+
+
+ <>
+
+
+ User Mode feature enables non-admin users to see namespaces they have access to.{" "}
+ This is achieved by configuring RBAC rules so that every authenticated user is granted to list namespaces.
+
+ >
+
+
+ );
}
}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/general.tsx b/src/renderer/components/+cluster-settings/general.tsx
index e03c6e2195..5fb6e9b81f 100644
--- a/src/renderer/components/+cluster-settings/general.tsx
+++ b/src/renderer/components/+cluster-settings/general.tsx
@@ -15,8 +15,6 @@ export class General extends React.Component {
render() {
return
General
-
-
diff --git a/src/renderer/components/+cluster-settings/removal.tsx b/src/renderer/components/+cluster-settings/removal.tsx
index f1d613c694..7d97e9c515 100644
--- a/src/renderer/components/+cluster-settings/removal.tsx
+++ b/src/renderer/components/+cluster-settings/removal.tsx
@@ -3,16 +3,18 @@ import { Cluster } from "../../../main/cluster";
import { RemoveClusterButton } from "./components/remove-cluster-button";
interface Props {
- cluster: Cluster;
+ cluster: Cluster;
}
export class Removal extends React.Component
{
render() {
const { cluster } = this.props;
- return
-
Removal
-
- ;
+ return (
+
+
Removal
+
+
+ );
}
}
\ No newline at end of file
diff --git a/src/renderer/components/+cluster-settings/status.tsx b/src/renderer/components/+cluster-settings/status.tsx
index 6e0bf528fc..ff0436598c 100644
--- a/src/renderer/components/+cluster-settings/status.tsx
+++ b/src/renderer/components/+cluster-settings/status.tsx
@@ -1,41 +1,40 @@
import React from "react";
-import { Spinner } from "../spinner";
import { Cluster } from "../../../main/cluster";
+import { SubTitle } from "../layout/sub-title";
+import { Table, TableCell, TableRow } from "../table";
interface Props {
cluster: Cluster;
}
export class Status extends React.Component {
- renderStatusRows(): JSX.Element[] {
+ renderStatusRows() {
const { cluster } = this.props;
-
- const rows: [string, React.ReactNode][] = [
- ["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"}`],
+ const rows = [
+ ["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"})`],
["Distribution", cluster.distribution],
["Kerbel Version", cluster.version],
["API Address", cluster.apiUrl],
+ ["Nodes Count", cluster.nodes || "0"]
];
-
- if (cluster.nodes > 0) {
- rows.push(["Nodes Count", cluster.nodes]);
- }
-
- return rows
- .map(([header, value]) => [
- {header} ,
- {value}
- ])
- .flat();
+ return (
+
+ {rows.map(([name, value]) => {
+ return (
+
+ {name}
+ {value}
+
+ );
+ })}
+
+ );
}
render() {
- const { cluster } = this.props;
-
return
Status
-
-
Cluster status
+
Cluster status information including: detected distribution, kernel version, and online status.
diff --git a/src/renderer/components/+config-resource-quotas/resource-quota-details.tsx b/src/renderer/components/+config-resource-quotas/resource-quota-details.tsx
index 6a2665e741..3da8c7112a 100644
--- a/src/renderer/components/+config-resource-quotas/resource-quota-details.tsx
+++ b/src/renderer/components/+config-resource-quotas/resource-quota-details.tsx
@@ -4,7 +4,7 @@ import kebabCase from "lodash/kebabCase";
import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { DrawerItem, DrawerTitle } from "../drawer";
-import { cpuUnitsToNumber, cssNames, unitsToBytes } from "../../utils";
+import { cpuUnitsToNumber, cssNames, unitsToBytes, metricUnitsToNumber } from "../../utils";
import { KubeObjectDetailsProps } from "../kube-object";
import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { LineProgress } from "../line-progress";
@@ -15,24 +15,30 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta";
interface Props extends KubeObjectDetailsProps
{
}
-@observer
-export class ResourceQuotaDetails extends React.Component {
- renderQuotas = (quota: ResourceQuota) => {
- const { hard, used } = quota.status
- if (!hard || !used) return null
- const transformUnit = (name: string, value: string) => {
- if (name.includes("memory") || name.includes("storage")) {
- return unitsToBytes(value)
- }
- if (name.includes("cpu")) {
- return cpuUnitsToNumber(value)
- }
- return parseInt(value)
- }
- return Object.entries(hard).map(([name, value]) => {
- if (!used[name]) return null
+const onlyNumbers = /$[0-9]*^/g;
+
+function transformUnit(name: string, value: string): number {
+ if (name.includes("memory") || name.includes("storage")) {
+ return unitsToBytes(value)
+ }
+
+ if (name.includes("cpu")) {
+ return cpuUnitsToNumber(value)
+ }
+
+ return metricUnitsToNumber(value);
+}
+
+function renderQuotas(quota: ResourceQuota): JSX.Element[] {
+ const { hard = {}, used = {} } = quota.status
+
+ return Object.entries(hard)
+ .filter(([name]) => used[name])
+ .map(([name, value]) => {
const current = transformUnit(name, used[name])
const max = transformUnit(name, value)
+ const usage = max === 0 ? 100 : Math.ceil(current / max * 100); // special case 0 max as always 100% usage
+
return (
{name}
@@ -41,14 +47,16 @@ export class ResourceQuotaDetails extends React.Component
{
max={max}
value={current}
tooltip={
- Set : {value}. Used : {Math.ceil(current / max * 100) + "%"}
+ Set : {value}. Usage : {usage + "%"}
}
/>
)
})
- }
+}
+@observer
+export class ResourceQuotaDetails extends React.Component {
render() {
const { object: quota } = this.props;
if (!quota) return null;
@@ -57,7 +65,7 @@ export class ResourceQuotaDetails extends React.Component {
Quotas} className="quota-list">
- {this.renderQuotas(quota)}
+ {renderQuotas(quota)}
{quota.getScopeSelector().length > 0 && (
diff --git a/src/renderer/components/+network-services/service-details.tsx b/src/renderer/components/+network-services/service-details.tsx
index 0c158c9e6b..df37d6238e 100644
--- a/src/renderer/components/+network-services/service-details.tsx
+++ b/src/renderer/components/+network-services/service-details.tsx
@@ -11,7 +11,7 @@ import { Service, serviceApi, endpointApi } from "../../api/endpoints";
import { _i18n } from "../../i18n";
import { apiManager } from "../../api/api-manager";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
-import { ServicePorts } from "./service-ports";
+import { ServicePortComponent } from "./service-port-component";
import { endpointStore } from "../+network-endpoints/endpoints.store";
import { ServiceDetailsEndpoint } from "./service-details-endpoint";
@@ -61,7 +61,13 @@ export class ServiceDetails extends React.Component {
)}
Ports}>
-
+
+ {
+ service.getPorts().map((port) => (
+
+ ))
+ }
+
{spec.type === "LoadBalancer" && spec.loadBalancerIP && (
diff --git a/src/renderer/components/+network-services/service-port-component.scss b/src/renderer/components/+network-services/service-port-component.scss
new file mode 100644
index 0000000000..0e9945631d
--- /dev/null
+++ b/src/renderer/components/+network-services/service-port-component.scss
@@ -0,0 +1,22 @@
+.ServicePortComponent {
+ &.waiting {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+
+ &:not(:last-child) {
+ margin-bottom: $margin;
+ }
+
+ span {
+ cursor: pointer;
+ color: $primary;
+ text-decoration: underline;
+ }
+
+ .Spinner {
+ --spinner-size: #{$unit * 2};
+ margin-left: $margin;
+ position: absolute;
+ }
+}
diff --git a/src/renderer/components/+network-services/service-port-component.tsx b/src/renderer/components/+network-services/service-port-component.tsx
new file mode 100644
index 0000000000..252bf8eb16
--- /dev/null
+++ b/src/renderer/components/+network-services/service-port-component.tsx
@@ -0,0 +1,48 @@
+import "./service-port-component.scss"
+
+import React from "react";
+import { observer } from "mobx-react";
+import { t } from "@lingui/macro";
+import { Service, ServicePort } from "../../api/endpoints";
+import { _i18n } from "../../i18n";
+import { apiBase } from "../../api"
+import { observable } from "mobx";
+import { cssNames } from "../../utils";
+import { Notifications } from "../notifications";
+import { Spinner } from "../spinner"
+
+interface Props {
+ service: Service;
+ port: ServicePort;
+}
+
+@observer
+export class ServicePortComponent extends React.Component {
+ @observable waiting = false;
+
+ async portForward() {
+ const { service, port } = this.props;
+ this.waiting = true;
+ try {
+ await apiBase.post(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}`, {})
+ } catch(error) {
+ Notifications.error(error);
+ } finally {
+ this.waiting = false;
+ }
+ }
+
+ render() {
+ const { port } = this.props;
+ return (
+
+ this.portForward() }>
+ {port.toString()}
+ {this.waiting && (
+
+ )}
+
+
+ );
+ }
+}
diff --git a/src/renderer/components/+network-services/service-ports.scss b/src/renderer/components/+network-services/service-ports.scss
deleted file mode 100644
index 5a683af86c..0000000000
--- a/src/renderer/components/+network-services/service-ports.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-.ServicePorts {
- &.waiting {
- opacity: 0.5;
- pointer-events: none;
- }
-
- p {
- &:not(:last-child) {
- margin-bottom: $margin;
- }
-
- span {
- cursor: pointer;
- color: $primary;
- text-decoration: underline;
- }
- }
-
- .Spinner {
- --spinner-size: #{$unit * 2};
- margin-left: $margin;
- position: absolute;
- }
-}
diff --git a/src/renderer/components/+network-services/service-ports.tsx b/src/renderer/components/+network-services/service-ports.tsx
deleted file mode 100644
index 3335be6907..0000000000
--- a/src/renderer/components/+network-services/service-ports.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import "./service-ports.scss"
-
-import React from "react";
-import { observer } from "mobx-react";
-import { t } from "@lingui/macro";
-import { Service, ServicePort } from "../../api/endpoints";
-import { _i18n } from "../../i18n";
-import { apiBase } from "../../api"
-import { observable } from "mobx";
-import { cssNames } from "../../utils";
-import { Notifications } from "../notifications";
-import { Spinner } from "../spinner"
-
-interface Props {
- service: Service;
-}
-
-@observer
-export class ServicePorts extends React.Component {
- @observable waiting = false;
-
- async portForward(port: ServicePort) {
- const { service } = this.props;
- this.waiting = true;
- apiBase.post(`/services/${service.getNs()}/${service.getName()}/port-forward/${port.port}`, {})
- .catch(error => {
- Notifications.error(error);
- })
- .finally(() => {
- this.waiting = false;
- });
- }
-
- render() {
- const { service } = this.props;
- return (
-
- {
- service.getPorts().map((port) => {
- return(
-
- this.portForward(port) }>
- {port.toString()}
- {this.waiting && (
-
- )}
-
-
- );
- })}
-
- );
- }
-}
diff --git a/src/renderer/components/+preferences/preferences.scss b/src/renderer/components/+preferences/preferences.scss
index 859315cc8a..94c08e8f53 100644
--- a/src/renderer/components/+preferences/preferences.scss
+++ b/src/renderer/components/+preferences/preferences.scss
@@ -1,23 +1,51 @@
.Preferences {
- h2 {
- &:not(:first-child) {
- margin-top: $padding * 3;
+ position: fixed!important; // Allows to cover ClustersMenu
+ z-index: 1;
+
+ .WizardLayout {
+ grid-template-columns: unset;
+ grid-template-rows: 76px 1fr;
+ padding: 0;
+
+ .content-col {
+ background-color: transparent;
+ padding: $padding * 8 0;
+
+ h2 {
+ margin-bottom: $margin * 2;
+
+ &:not(:first-child) {
+ margin-top: $margin * 3;
+ }
+ }
+
+ .repos {
+ position: relative;
+
+ .Badge {
+ display: flex;
+ margin: 0;
+ margin-bottom: 1px;
+ padding: $padding $padding * 2;
+ }
+ }
+
+ .hint {
+ margin-top: -$margin;
+ }
}
}
- .info-block {
- --flex-gap: #{$padding};
+ .is-mac & {
+ .WizardLayout .head-col {
+ padding-top: 32px;
+ overflow: hidden;
+ }
}
- .repos {
- --flex-gap: #{$padding};
-
- > .title {
- font-style: italic;
- }
-
- .Badge {
- margin: $padding / 2;
+ .Select {
+ &__control {
+ box-shadow: 0 0 0 1px $borderFaintColor;
}
}
}
\ No newline at end of file
diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx
index 02e66b7342..fc91952213 100644
--- a/src/renderer/components/+preferences/preferences.tsx
+++ b/src/renderer/components/+preferences/preferences.tsx
@@ -1,5 +1,5 @@
import "./preferences.scss"
-import React, { Fragment } from "react";
+import React from "react";
import { observer } from "mobx-react";
import { action, computed, observable } from "mobx";
import { t, Trans } from "@lingui/macro";
@@ -15,11 +15,12 @@ import { Notifications } from "../notifications";
import { Badge } from "../badge";
import { Spinner } from "../spinner";
import { themeStore } from "../../theme.store";
+import { history } from "../../navigation";
+import { Tooltip } from "../tooltip";
@observer
export class Preferences extends React.Component {
@observable helmLoading = false;
- @observable helmUpdating = false;
@observable helmRepos: HelmRepo[] = [];
@observable helmAddedRepos = observable.map();
@@ -88,9 +89,9 @@ export class Preferences extends React.Component {
Notifications.ok(Helm branch {repo.name} already in use )
return;
}
- this.helmUpdating = false;
+ this.helmLoading = true;
await this.addRepo(repo);
- this.helmUpdating = false;
+ this.helmLoading = false;
}
formatHelmOptionLabel = ({ value: repo }: SelectOption) => {
@@ -103,104 +104,95 @@ export class Preferences extends React.Component {
)
}
- renderInfo() {
- return (
-
-
- Preferences
-
-
-
-
- Lens Global Settings (applicable to all clusters )
-
-
-
- )
- }
-
render() {
const { preferences } = userStore;
+ const header = (
+ <>
+ Preferences
+
+ >
+ );
return (
-
- Color Theme
- preferences.colorTheme = value}
- />
+
+
+ Color Theme
+ preferences.colorTheme = value}
+ />
- Download Mirror
- Download mirror for kubectl}
- options={this.downloadMirrorOptions}
- value={preferences.downloadMirror}
- onChange={({ value }: SelectOption) => preferences.downloadMirror = value}
- />
+ Download Mirror
+ Download mirror for kubectl}
+ options={this.downloadMirrorOptions}
+ value={preferences.downloadMirror}
+ onChange={({ value }: SelectOption) => preferences.downloadMirror = value}
+ />
- Helm
- Repositories}
- isLoading={this.helmLoading}
- isDisabled={this.helmUpdating}
- options={this.helmOptions}
- onChange={this.onRepoSelect}
- formatOptionLabel={this.formatHelmOptionLabel}
- controlShouldRenderValue={false}
- />
-
-
- Added repos:
-
-
- {this.helmLoading &&
}
+
Helm
+
Repositories}
+ isLoading={this.helmLoading}
+ isDisabled={this.helmLoading}
+ options={this.helmOptions}
+ onChange={this.onRepoSelect}
+ formatOptionLabel={this.formatHelmOptionLabel}
+ controlShouldRenderValue={false}
+ />
+
{Array.from(this.helmAddedRepos).map(([name, repo]) => {
+ const tooltipId = `message-${name}`;
return (
-
- {name}
+
+ {name}
this.removeRepo(repo)}
tooltip={Remove }
/>
+
+ {repo.url}
+
)
})}
-
-
HTTP Proxy
-
preferences.httpsProxy = v}
- />
-
- Proxy is used only for non-cluster communication.
-
+
HTTP Proxy
+
preferences.httpsProxy = v}
+ />
+
+ Proxy is used only for non-cluster communication.
+
-
Certificate Trust
-
Allow untrusted Certificate Authorities}
- value={preferences.allowUntrustedCAs}
- onChange={v => preferences.allowUntrustedCAs = v}
- />
-
- This will make Lens to trust ANY certificate authority without any validations. {" "}
- Needed with some corporate proxies that do certificate re-writing. {" "}
- Does not affect cluster communications!
-
+ Certificate Trust
+ Allow untrusted Certificate Authorities}
+ value={preferences.allowUntrustedCAs}
+ onChange={v => preferences.allowUntrustedCAs = v}
+ />
+
+ This will make Lens to trust ANY certificate authority without any validations. {" "}
+ Needed with some corporate proxies that do certificate re-writing. {" "}
+ Does not affect cluster communications!
+
- Telemetry & Usage Tracking
- Allow telemetry & usage tracking}
- value={preferences.allowTelemetry}
- onChange={v => preferences.allowTelemetry = v}
- />
-
- Telemetry & usage data is collected to continuously improve the Lens experience.
-
-
- )
+ Telemetry & Usage Tracking
+ Allow telemetry & usage tracking}
+ value={preferences.allowTelemetry}
+ onChange={v => preferences.allowTelemetry = v}
+ />
+
+ Telemetry & usage data is collected to continuously improve the Lens experience.
+
+
+
+ );
}
}
diff --git a/src/renderer/components/+whats-new/whats-new.tsx b/src/renderer/components/+whats-new/whats-new.tsx
index 9bde7da141..c63e8f25d1 100644
--- a/src/renderer/components/+whats-new/whats-new.tsx
+++ b/src/renderer/components/+whats-new/whats-new.tsx
@@ -7,12 +7,11 @@ import { userStore } from "../../../common/user-store"
import { navigate } from "../../navigation";
import { Button } from "../button";
import { Trans } from "@lingui/macro";
-import { staticDir } from "../../../common/vars";
import marked from "marked"
@observer
export class WhatsNew extends React.Component {
- releaseNotes = fs.readFileSync(path.join(staticDir, "RELEASE_NOTES.md")).toString();
+ releaseNotes = fs.readFileSync(path.join(__static, "RELEASE_NOTES.md")).toString();
ok = () => {
navigate("/");
diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx
index 32fb1ec10c..5a3031787a 100644
--- a/src/renderer/components/+workloads-deployments/deployments.tsx
+++ b/src/renderer/components/+workloads-deployments/deployments.tsx
@@ -98,7 +98,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps) {
return (
DeploymentScaleDialog.open(object)}>
-
+
Scale
diff --git a/src/renderer/components/+workloads-pods/pod-container-port.scss b/src/renderer/components/+workloads-pods/pod-container-port.scss
new file mode 100644
index 0000000000..081f0b1090
--- /dev/null
+++ b/src/renderer/components/+workloads-pods/pod-container-port.scss
@@ -0,0 +1,23 @@
+.PodContainerPort {
+ &.waiting {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+
+ &:not(:last-child) {
+ margin-bottom: $margin;
+ }
+
+ span {
+ cursor: pointer;
+ color: $primary;
+ text-decoration: underline;
+ position: relative;
+ }
+
+ .Spinner {
+ --spinner-size: #{$unit * 2};
+ margin-left: $margin;
+ position: absolute;
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/components/+workloads-pods/pod-container-port.tsx b/src/renderer/components/+workloads-pods/pod-container-port.tsx
new file mode 100644
index 0000000000..9ebaed4fd7
--- /dev/null
+++ b/src/renderer/components/+workloads-pods/pod-container-port.tsx
@@ -0,0 +1,54 @@
+import "./pod-container-port.scss"
+
+import React from "react";
+import { observer } from "mobx-react";
+import { t } from "@lingui/macro";
+import { Pod, IPodContainer } from "../../api/endpoints";
+import { _i18n } from "../../i18n";
+import { apiBase } from "../../api"
+import { observable } from "mobx";
+import { cssNames } from "../../utils";
+import { Notifications } from "../notifications";
+import { Spinner } from "../spinner"
+
+interface Props {
+ pod: Pod;
+ port: {
+ name?: string;
+ containerPort: number;
+ protocol: string;
+ }
+}
+
+@observer
+export class PodContainerPort extends React.Component {
+ @observable waiting = false;
+
+ async portForward() {
+ const { pod, port } = this.props;
+ this.waiting = true;
+ try {
+ await apiBase.post(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}`, {})
+ } catch(error) {
+ Notifications.error(error);
+ } finally {
+ this.waiting = false;
+ }
+ }
+
+ render() {
+ const { port } = this.props;
+ const { name, containerPort, protocol } = port;
+ const text = (name ? name + ': ' : '')+`${containerPort}/${protocol}`
+ return (
+
+ this.portForward() }>
+ {text}
+ {this.waiting && (
+
+ )}
+
+
+ )
+ }
+}
diff --git a/src/renderer/components/+workloads-pods/pod-details-container.tsx b/src/renderer/components/+workloads-pods/pod-details-container.tsx
index a5cdcc1fbd..79180b1130 100644
--- a/src/renderer/components/+workloads-pods/pod-details-container.tsx
+++ b/src/renderer/components/+workloads-pods/pod-details-container.tsx
@@ -8,6 +8,7 @@ import { cssNames } from "../../utils";
import { StatusBrick } from "../status-brick";
import { Badge } from "../badge";
import { ContainerEnvironment } from "./pod-container-env";
+import { PodContainerPort } from "./pod-container-port";
import { ResourceMetrics } from "../resource-metrics";
import { IMetrics } from "../../api/endpoints/metrics.api";
import { ContainerCharts } from "./container-charts";
@@ -64,13 +65,10 @@ export class PodDetailsContainer extends React.Component {
{ports && ports.length > 0 &&
Ports}>
{
- ports.map(port => {
- const { name, containerPort, protocol } = port;
- const key = `${container.name}-port-${containerPort}-${protocol}`
- return (
-
- {name ? name + ': ' : ''}{containerPort}/{protocol}
-
+ ports.map((port) => {
+ const key = `${container.name}-port-${port.containerPort}-${port.protocol}`
+ return(
+
)
})
}
diff --git a/src/renderer/components/+workloads-pods/pod-logs-dialog.tsx b/src/renderer/components/+workloads-pods/pod-logs-dialog.tsx
index e6e9708002..d7ff3863cc 100644
--- a/src/renderer/components/+workloads-pods/pod-logs-dialog.tsx
+++ b/src/renderer/components/+workloads-pods/pod-logs-dialog.tsx
@@ -226,7 +226,7 @@ export class PodLogsDialog extends React.Component {
tooltip={(showTimestamps ? _i18n._(t`Hide`) : _i18n._(t`Show`)) + " " + _i18n._(t`timestamps`)}
/>
diff --git a/src/renderer/components/app-init/app-init.tsx b/src/renderer/components/app-init/app-init.tsx
index fe46a8a6d5..e5a740ebc4 100644
--- a/src/renderer/components/app-init/app-init.tsx
+++ b/src/renderer/components/app-init/app-init.tsx
@@ -13,7 +13,6 @@ interface Props {
export class AppInit extends React.Component {
static async start(rootElem: HTMLElement) {
-
render( , rootElem); // show loading indicator asap
await AppInit.readyStateCheck(rootElem); // wait while all good to run
}
diff --git a/src/renderer/components/app.scss b/src/renderer/components/app.scss
index 3fcbce9613..59d9b632d0 100755
--- a/src/renderer/components/app.scss
+++ b/src/renderer/components/app.scss
@@ -82,7 +82,7 @@ hr {
h1 {
color: white;
font-size: 28px;
- font-weight: 300;
+ font-weight: normal;
letter-spacing: -.010em;
margin: 0;
}
@@ -99,13 +99,13 @@ h3 {
h4 {
@extend h3;
- font-size: 16px;
+ font-size: 18px;
}
h5 {
@extend h4;
padding: $padding / 2 0;
- font-size: 14px;
+ font-size: 16px;
}
h6 {
diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx
index c51afbdc14..0c3044fc12 100755
--- a/src/renderer/components/app.tsx
+++ b/src/renderer/components/app.tsx
@@ -28,14 +28,21 @@ import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale
import { CustomResources } from "./+custom-resources/custom-resources";
import { crdRoute } from "./+custom-resources";
import { isAllowedResource } from "../../common/rbac";
-import { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings";
import { ErrorBoundary } from "./error-boundary";
import { Terminal } from "./dock/terminal";
+import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store";
+import logger from "../../main/logger";
+import { clusterIpc } from "../../common/cluster-ipc";
+import { webFrame } from "electron";
@observer
export class App extends React.Component {
static async init() {
+ const clusterId = getHostedClusterId();
+ logger.info(`[APP]: Init dashboard, clusterId=${clusterId}`)
await Terminal.preloadFonts()
+ await clusterIpc.init.invokeFromRenderer(clusterId, webFrame.routingId);
+ await getHostedCluster().whenInitialized;
}
get startURL() {
@@ -52,7 +59,6 @@ export class App extends React.Component {
-
@@ -66,9 +72,9 @@ export class App extends React.Component {
-
+
diff --git a/src/renderer/components/cluster-icon/cluster-icon.scss b/src/renderer/components/cluster-icon/cluster-icon.scss
index f7e159a7c5..b8a87a07de 100644
--- a/src/renderer/components/cluster-icon/cluster-icon.scss
+++ b/src/renderer/components/cluster-icon/cluster-icon.scss
@@ -7,6 +7,12 @@
user-select: none;
cursor: pointer;
+ &.interactive {
+ img {
+ opacity: .55;
+ }
+ }
+
&.active, &.interactive:hover {
background-color: #fff;
@@ -16,7 +22,6 @@
}
img {
- opacity: .55;
width: var(--size);
height: var(--size);
}
diff --git a/src/renderer/components/cluster-icon/cluster-icon.tsx b/src/renderer/components/cluster-icon/cluster-icon.tsx
index 9d9a359670..ccc65892de 100644
--- a/src/renderer/components/cluster-icon/cluster-icon.tsx
+++ b/src/renderer/components/cluster-icon/cluster-icon.tsx
@@ -42,12 +42,12 @@ export class ClusterIcon extends React.Component {
active: isActive,
});
return (
-