From 0f4248de689daddaf0b71cd00d88776b0c6fc74a Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Fri, 7 Aug 2020 15:57:16 +0300 Subject: [PATCH] Fixing Cluster Settings layout (#651) * A bit of cleaning in Add Cluster page Signed-off-by: alexfront * Adding head-col to WizardLayout Signed-off-by: alexfront * Fixing Cluster Settings general layout bugs Signed-off-by: alexfront * Cluster Status view refactoring Signed-off-by: alexfront * Install Metrics component refactoring Using notifications for error, removed picking button icon method, simplified button generation. Signed-off-by: alexfront * Remove icons / checks from RemoveClusterButton Signed-off-by: alexfront * Fixing colorError in Input styles Signed-off-by: alexfront * Preventing Input's spellchecking Signed-off-by: alexfront * ClusterNameSettings refactoring Signed-off-by: alexfront * ClusterWorkspaceSettings refactoring/fixing Signed-off-by: alexfront * ClusterProxySetting refactoring Signed-off-by: alexfront * ClusterPrometheusSetting refactoring Signed-off-by: alexfront * Clean up Removal section Signed-off-by: alexfront * Glued InstallMetrics & InstallUserMode into 1 component Signed-off-by: alexfront * Removing unused styles in Cluster Settings Signed-off-by: alexfront * Cluster Settings styling Signed-off-by: alexfront * Adding close button to settings header Signed-off-by: alexfront * ClusterHomeDirSetting refactoring Signed-off-by: alexfront * FilePicker restyling Signed-off-by: alexfront * Fixing Prometheus selector Signed-off-by: alexfront * Fixing Hashicon Passing cluster name instead of cluster id to prevent icon changing while typing new cluster name Signed-off-by: alexfront * Minor ClusterSettings fixes Signed-off-by: alexfront * Increasing opacity for non-interactive icons Signed-off-by: alexfront * Keep feature install loading state Waiting for props to change before disabling loading state (gray button width spinner) Signed-off-by: alexfront * Remove arrays in disposeOnUnmount() Signed-off-by: alexfront * Fix Cluster select behavior Now clicking cluster icon in sidebar always leads to / dashboard. And 'Settings' submenu switches active cluster at first and only the showing Cluster Settings Signed-off-by: alexfront * Using structuralComparator in feature installer Signed-off-by: alexfront * Saving input fields on blur Signed-off-by: alexfront * Setting Select color same as Input color Signed-off-by: alexfront --- src/common/cluster-store.ts | 4 + .../components/+add-cluster/add-cluster.tsx | 30 ++-- .../+cluster-settings/cluster-settings.scss | 137 +++++++++--------- .../+cluster-settings/cluster-settings.tsx | 44 ++++-- .../components/cluster-home-dir-setting.tsx | 101 ++++--------- .../components/cluster-icon-setting.tsx | 64 ++++---- .../components/cluster-name-setting.tsx | 93 +++--------- .../components/cluster-prometheus-setting.tsx | 55 +++---- .../components/cluster-proxy-setting.tsx | 118 ++++----------- .../components/cluster-workspace-setting.tsx | 50 +++---- .../components/install-feature.tsx | 93 ++++++++++++ .../components/install-metrics.tsx | 109 -------------- .../components/install-user-mode.tsx | 108 -------------- .../components/remove-cluster-button.tsx | 58 ++------ .../+cluster-settings/components/statuses.ts | 24 --- .../components/+cluster-settings/features.tsx | 36 ++++- .../components/+cluster-settings/general.tsx | 2 - .../components/+cluster-settings/removal.tsx | 12 +- .../components/+cluster-settings/status.tsx | 37 +++-- .../components/cluster-icon/cluster-icon.scss | 7 +- .../components/cluster-icon/cluster-icon.tsx | 4 +- .../cluster-manager/clusters-menu.tsx | 12 +- .../components/file-picker/file-picker.scss | 19 +-- .../components/file-picker/file-picker.tsx | 6 +- src/renderer/components/input/input.scss | 2 +- src/renderer/components/input/input.tsx | 1 + .../components/layout/wizard-layout.scss | 9 ++ .../components/layout/wizard-layout.tsx | 9 +- src/renderer/components/select/select.scss | 6 +- 29 files changed, 497 insertions(+), 753 deletions(-) create mode 100644 src/renderer/components/+cluster-settings/components/install-feature.tsx delete mode 100644 src/renderer/components/+cluster-settings/components/install-metrics.tsx delete mode 100644 src/renderer/components/+cluster-settings/components/install-user-mode.tsx delete mode 100644 src/renderer/components/+cluster-settings/components/statuses.ts diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index e4febf532a..efed98d00a 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -86,6 +86,10 @@ export class ClusterStore extends BaseStore { return Array.from(this.clusters.values()); } + setActive(id: ClusterId) { + this.activeClusterId = id; + } + hasClusters() { return this.clusters.size > 0; } diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 221f695425..93d28d400a 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -124,7 +124,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 +137,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 diff --git a/src/renderer/components/+cluster-settings/cluster-settings.scss b/src/renderer/components/+cluster-settings/cluster-settings.scss index 1ea6926eab..e02a4da5f8 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.scss +++ b/src/renderer/components/+cluster-settings/cluster-settings.scss @@ -1,86 +1,85 @@ .ClusterSettings { - overflow-y: scroll; - grid-template-columns: unset; + grid-template-columns: unset; + padding: 0; - .info-col { - display: none; + .head-col { + justify-content: space-between; + + :nth-child(2) { + flex: 1 0 0; } - .content-col { - margin-right: unset; + a { + text-decoration: none; + color: $grey-600; + } + } + + .info-col { + display: none; + } + + .content-col { + margin: 0; + padding-top: $padding * 3; + background-color: transparent; + + .SubTitle { + text-transform: none; } - * { - margin-top: 40px; + .settings-wrapper { + margin: 0 auto; + width: 60%; + min-width: 570px; + max-width: 1000px; - &:first-child { - margin-top: 0px; - } - } + > div { + margin-top: $margin * 5; + } - h4 { - margin-top: 20px; - } - - .status-table { - margin-top: 20px; - display: grid; - grid-template-columns: 1fr 3fr; - grid-gap: 10px; - } - - .loading { - margin-top: 20px; - text-align: center; - - .Spinner { - display: inline-block; - } - } - - .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 { + .admin-note { font-size: small; - color: #707070; + opacity: 0.5; + margin-left: $margin; + } + + .button-area { + margin-top: $margin * 2; + } } - input[type="text"] { - color: white; + .file-loader { + margin-top: $margin * 2; } - button { - margin-top: 5px; + .hint { + font-size: smaller; + opacity: 0.8; + } + } - .Spinner { - width: 10px; - height: 10px; - border-color: transparent black; + .status-table { + margin: $margin * 3 0; + + .Table { + border: 1px solid var(--drawerSubtitleBackground); + border-radius: $radius; + + .TableRow { + &:not(:last-of-type) { + border-bottom: 1px solid var(--drawerSubtitleBackground); } + + .value { + flex-grow: 2; + color: var(--textColorSecondary); + } + } } + } + + .Input,.Select { + margin-top: 10px; + } } \ 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 777562060b..4ce4dcb379 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx @@ -1,25 +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 { getHostedCluster } from "../../../common/cluster-store"; import { WizardLayout } from "../layout/wizard-layout"; +import { ClusterIcon } from "../cluster-icon"; +import { Icon } from "../icon"; @observer export class ClusterSettings extends React.Component { render() { const cluster = getHostedCluster(); - + const header = ( + <> + +

{cluster.preferences.clusterName}

+ + + + + ); 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 + if (this.props.cluster.preferences.icon) { + return } } 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..e4f81bb18a 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,44 @@ 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 merge from "lodash/merge"; 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"; -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.

- { + const provider = { + prometheusProvider: { + type: value + } + } + merge(this.props.cluster.preferences, provider); + }} + options={options} + /> + + ); } } \ 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:

- 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 && + + } + {features.installed && + + } + {!features.installed && !features.canUpgrade && + + } + {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( - - ); - } - - if (cluster.features[MetricsFeature.id]?.installed) { - buttons.push( - - ); - } else { - buttons.push( - - ); - } - - 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( - - ); - } - - if (cluster.features[UserModeFeature.id]?.installed) { - buttons.push( - - ); - } else { - buttons.push( - - ); - } - - 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 ( -
- -
- ); - } - - 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 ( + + ); + } } \ 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..3136fc5962 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][] = [ + 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/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 ( -
+
{showTooltip && ( {clusterName} )} {icon && {clusterName}/} - {!icon && } + {!icon && } {showErrors && isAdmin && eventCount > 0 && ( { @observable showHint = true; showCluster = (clusterId: ClusterId) => { - if (clusterStore.activeClusterId === clusterId) { - navigate("/"); // redirect to index - } else { - clusterStore.activeClusterId = clusterId; - } + clusterStore.setActive(clusterId); + navigate("/"); // redirect to index } addCluster = () => { @@ -50,7 +47,10 @@ export class ClustersMenu extends React.Component { menu.append(new MenuItem({ label: _i18n._(t`Settings`), - click: () => navigate(clusterSettingsURL()) + click: () => { + clusterStore.setActive(cluster.id); + navigate(clusterSettingsURL()) + } })); if (cluster.online) { menu.append(new MenuItem({ diff --git a/src/renderer/components/file-picker/file-picker.scss b/src/renderer/components/file-picker/file-picker.scss index dbfada6c8e..63a9c2da74 100644 --- a/src/renderer/components/file-picker/file-picker.scss +++ b/src/renderer/components/file-picker/file-picker.scss @@ -1,14 +1,11 @@ .FilePicker { - input[type="file"] { - display: none; - } + input[type="file"] { + display: none; + } - label { - display: inline-block; - border: medium solid; - padding: 10px; - border-radius: 5px; - cursor: pointer; - margin: 5px; - } + label { + display: inline-flex; + cursor: pointer; + color: var(--blue); + } } \ No newline at end of file diff --git a/src/renderer/components/file-picker/file-picker.tsx b/src/renderer/components/file-picker/file-picker.tsx index 0290b5e645..5af7a176d1 100644 --- a/src/renderer/components/file-picker/file-picker.tsx +++ b/src/renderer/components/file-picker/file-picker.tsx @@ -43,7 +43,7 @@ export enum OverTotalSizeLimitStyle { export interface BaseProps { accept?: string; - labelText: string; + label: React.ReactNode; multiple?: boolean; // limit is the optional maximum number of files to upload @@ -175,10 +175,10 @@ export class FilePicker extends React.Component { } render() { - const { accept, labelText, multiple } = this.props; + const { accept, label, multiple } = this.props; return
- + { rows: multiLine ? (rows || 1) : null, ref: this.bindRef, type: "text", + spellCheck: "false", }); return ( diff --git a/src/renderer/components/layout/wizard-layout.scss b/src/renderer/components/layout/wizard-layout.scss index 5d7b95bbab..a81cdfd429 100644 --- a/src/renderer/components/layout/wizard-layout.scss +++ b/src/renderer/components/layout/wizard-layout.scss @@ -13,6 +13,11 @@ padding: $spacing; } + > .head-col { + position: sticky; + border-bottom: 1px solid $grey-800; + } + > .content-col { margin-right: $spacing; background-color: var(--clusters-menu-bgc); @@ -29,6 +34,10 @@ border-left: 1px solid #353a3e; } + p { + line-height: 140%; + } + a { color: $colorInfo; } diff --git a/src/renderer/components/layout/wizard-layout.tsx b/src/renderer/components/layout/wizard-layout.tsx index 107c8feffe..01b7f331d8 100644 --- a/src/renderer/components/layout/wizard-layout.tsx +++ b/src/renderer/components/layout/wizard-layout.tsx @@ -5,6 +5,8 @@ import { cssNames, IClassName } from "../../utils"; interface Props { className?: IClassName; + header?: React.ReactNode; + headerClass?: IClassName; contentClass?: IClassName; infoPanelClass?: IClassName; infoPanel?: React.ReactNode; @@ -13,9 +15,14 @@ interface Props { @observer export class WizardLayout extends React.Component { render() { - const { className, contentClass, infoPanelClass, infoPanel, children: content } = this.props; + const { className, contentClass, infoPanelClass, infoPanel, header, headerClass, children: content } = this.props; return (
+ {header && ( +
+ {header} +
+ )}
{content}
diff --git a/src/renderer/components/select/select.scss b/src/renderer/components/select/select.scss index 002c2a567c..20078215ba 100644 --- a/src/renderer/components/select/select.scss +++ b/src/renderer/components/select/select.scss @@ -31,7 +31,7 @@ html { border-radius: $radius; background: transparent; min-height: 0; - box-shadow: 0 0 0 1px $halfGray; + box-shadow: 0 0 0 1px $borderFaintColor; &--is-focused { box-shadow: 0 0 0 2px $primary; @@ -42,6 +42,10 @@ html { margin-bottom: 1px; } + &__single-value { + color: var(--textColorSecondary); + } + &__indicator { padding: $padding /2; opacity: .55;