Refactor cluster settings to catalog entity settings (#2525)

* fix cluster settings page layout

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* cleanup

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor cluster settings to pluggable entity settings

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix gh actions network timeout on yarn install

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* review changes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2021-04-20 07:05:44 +03:00
1 parent adec401acd
commit 8dde4a1ecb
51 files changed
+437 -336

No files matched your search

@@ -0,0 +1,16 @@
import { navigate } from "../../navigation";
import { commandRegistry } from "../../../extensions/registries/command-registry";
import { clusterStore } from "../../../common/cluster-store";
import { entitySettingsURL } from "../+entity-settings";
commandRegistry.add({
id: "cluster.viewCurrentClusterSettings",
title: "Cluster: View Settings",
scope: "global",
action: () => navigate(entitySettingsURL({
params: {
entityId: clusterStore.active.id
}
})),
isActive: (context) => !!context.entity
});
@@ -0,0 +1,141 @@
import React from "react";
import { clusterStore } from "../../../common/cluster-store";
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
import { ClusterNameSetting } from "./components/cluster-name-setting";
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
import { ClusterAccessibleNamespaces } from "./components/cluster-accessible-namespaces";
import { ClusterMetricsSetting } from "./components/cluster-metrics-setting";
import { ShowMetricsSetting } from "./components/show-metrics";
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
import { ClusterKubeconfig } from "./components/cluster-kubeconfig";
import { entitySettingRegistry } from "../../../extensions/registries";
import { CatalogEntity } from "../../api/catalog-entity";
function getClusterForEntity(entity: CatalogEntity) {
const cluster = clusterStore.getById(entity.metadata.uid);
if (!cluster?.enabled) {
return null;
}
return cluster;
}
entitySettingRegistry.add([
{
apiVersions: ["entity.k8slens.dev/v1alpha1"],
kind: "KubernetesCluster",
source: "local",
title: "General",
components: {
View: (props: { entity: CatalogEntity }) => {
const cluster = getClusterForEntity(props.entity);
if (!cluster) {
return null;
}
return (
<section>
<section>
<ClusterNameSetting cluster={cluster} />
</section>
<section>
<ClusterKubeconfig cluster={cluster} />
</section>
</section>
);
}
}
},
{
apiVersions: ["entity.k8slens.dev/v1alpha1"],
kind: "KubernetesCluster",
title: "Proxy",
components: {
View: (props: { entity: CatalogEntity }) => {
const cluster = getClusterForEntity(props.entity);
if (!cluster) {
return null;
}
return (
<section>
<ClusterProxySetting cluster={cluster} />
</section>
);
}
}
},
{
apiVersions: ["entity.k8slens.dev/v1alpha1"],
kind: "KubernetesCluster",
title: "Terminal",
components: {
View: (props: { entity: CatalogEntity }) => {
const cluster = getClusterForEntity(props.entity);
if (!cluster) {
return null;
}
return (
<section>
<ClusterHomeDirSetting cluster={cluster} />
</section>
);
}
}
},
{
apiVersions: ["entity.k8slens.dev/v1alpha1"],
kind: "KubernetesCluster",
title: "Namespaces",
components: {
View: (props: { entity: CatalogEntity }) => {
const cluster = getClusterForEntity(props.entity);
if (!cluster) {
return null;
}
return (
<section>
<ClusterAccessibleNamespaces cluster={cluster} />
</section>
);
}
}
},
{
apiVersions: ["entity.k8slens.dev/v1alpha1"],
kind: "KubernetesCluster",
source: "local",
title: "Metrics",
components: {
View: (props: { entity: CatalogEntity }) => {
const cluster = getClusterForEntity(props.entity);
if (!cluster) {
return null;
}
return (
<section>
<section>
<ClusterPrometheusSetting cluster={cluster} />
</section>
<section>
<ClusterMetricsSetting cluster={cluster}/>
</section>
<section>
<ShowMetricsSetting cluster={cluster}/>
</section>
</section>
);
}
}
}
]);
@@ -0,0 +1,38 @@
import React from "react";
import { observer } from "mobx-react";
import { Cluster } from "../../../../main/cluster";
import { SubTitle } from "../../layout/sub-title";
import { EditableList } from "../../editable-list";
import { observable } from "mobx";
interface Props {
cluster: Cluster;
}
@observer
export class ClusterAccessibleNamespaces extends React.Component<Props> {
@observable namespaces = new Set(this.props.cluster.accessibleNamespaces);
render() {
return (
<>
<SubTitle title="Accessible Namespaces" id="accessible-namespaces" />
<EditableList
placeholder="Add new namespace..."
add={(newNamespace) => {
this.namespaces.add(newNamespace);
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
}}
items={Array.from(this.namespaces)}
remove={({ oldItem: oldNamesapce }) => {
this.namespaces.delete(oldNamesapce);
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
}}
/>
<small className="hint">
This setting is useful for manually specifying which namespaces you have access to. This is useful when you do not have permissions to list namespaces.
</small>
</>
);
}
}
@@ -0,0 +1,50 @@
import React from "react";
import { observable, autorun } from "mobx";
import { observer, disposeOnUnmount } from "mobx-react";
import { Cluster } from "../../../../main/cluster";
import { Input } from "../../input";
import { SubTitle } from "../../layout/sub-title";
interface Props {
cluster: Cluster;
}
@observer
export class ClusterHomeDirSetting extends React.Component<Props> {
@observable directory = "";
componentDidMount() {
disposeOnUnmount(this,
autorun(() => {
this.directory = this.props.cluster.preferences.terminalCWD || "";
})
);
}
save = () => {
this.props.cluster.preferences.terminalCWD = this.directory;
};
onChange = (value: string) => {
this.directory = value;
};
render() {
return (
<>
<SubTitle title="Working Directory"/>
<Input
theme="round-black"
value={this.directory}
onChange={this.onChange}
onBlur={this.save}
placeholder="$HOME"
/>
<small className="hint">
An explicit start path where the terminal will be launched,{" "}
this is used as the current working directory (cwd) for the shell process.
</small>
</>
);
}
}
@@ -0,0 +1,34 @@
import React from "react";
import { Cluster } from "../../../../main/cluster";
import { observer } from "mobx-react";
import { SubTitle } from "../../layout/sub-title";
import { autobind } from "../../../../common/utils";
import { shell } from "electron";
interface Props {
cluster: Cluster;
}
@observer
export class ClusterKubeconfig extends React.Component<Props> {
@autobind()
openKubeconfig() {
const { cluster } = this.props;
shell.showItemInFolder(cluster.kubeConfigPath);
}
render() {
return (
<>
<SubTitle title="Kubeconfig" />
<span>
<a className="link value" onClick={this.openKubeconfig}>{this.props.cluster.kubeConfigPath}</a>
</span>
</>
);
}
}
@@ -0,0 +1,12 @@
.MetricsSelect {
$spacing: $padding;
--flex-gap: #{$spacing};
.Badge {
margin-top: $spacing;
}
.Button {
margin-top: $spacing;
}
}
@@ -0,0 +1,118 @@
import "./cluster-metrics-setting.scss";
import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
import { Select, SelectOption } from "../../select/select";
import { Icon } from "../../icon/icon";
import { Button } from "../../button/button";
import { SubTitle } from "../../layout/sub-title";
import { Cluster } from "../../../../main/cluster";
import { observable, reaction } from "mobx";
interface Props {
cluster: Cluster;
}
export enum ResourceType {
Cluster = "Cluster",
Node = "Node",
Pod = "Pod",
Deployment = "Deployment",
StatefulSet = "StatefulSet",
Container = "Container",
Ingress = "Ingress",
VolumeClaim = "VolumeClaim",
ReplicaSet = "ReplicaSet",
DaemonSet = "DaemonSet",
}
@observer
export class ClusterMetricsSetting extends React.Component<Props> {
@observable hiddenMetrics = observable.set<string>();
componentDidMount() {
this.hiddenMetrics = observable.set<string>(this.props.cluster.preferences.hiddenMetrics ?? []);
disposeOnUnmount(this, [
reaction(() => this.props.cluster.preferences.hiddenMetrics, () => {
this.hiddenMetrics = observable.set<string>(this.props.cluster.preferences.hiddenMetrics ?? []);
}),
]);
}
save = () => {
this.props.cluster.preferences.hiddenMetrics = Array.from(this.hiddenMetrics);
};
onChangeSelect = (values: SelectOption<ResourceType>[]) => {
for (const { value } of values) {
if (this.hiddenMetrics.has(value)) {
this.hiddenMetrics.delete(value);
} else {
this.hiddenMetrics.add(value);
}
}
this.save();
};
onChangeButton = () => {
Object.keys(ResourceType).map(value =>
this.hiddenMetrics.add(value)
);
this.save();
};
reset = () => {
this.hiddenMetrics.clear();
this.save();
};
formatOptionLabel = ({ value: resource }: SelectOption<ResourceType>) => (
<div className="flex gaps align-center">
<span>{resource}</span>
{this.hiddenMetrics.has(resource) && <Icon smallest material="check" className="box right" />}
</div>
);
renderMetricsSelect() {
return (
<>
<Select
className="box grow"
placeholder="Select metrics to hide..."
isMulti
isSearchable
onMenuClose={this.save}
closeMenuOnSelect={false}
controlShouldRenderValue={false}
options={Object.values(ResourceType)}
onChange={this.onChangeSelect}
formatOptionLabel={this.formatOptionLabel}
/>
<Button
primary
label="Hide all metrics"
onClick={this.onChangeButton}
/>
<Button
primary
label="Reset"
onClick={this.reset}
/>
</>
);
}
render() {
return (
<div className="MetricsSelect">
<SubTitle title={"Hide metrics from the UI"}/>
<div className="flex gaps">
{this.renderMetricsSelect()}
</div>
</div>
);
}
}
@@ -0,0 +1,47 @@
import React from "react";
import { Cluster } from "../../../../main/cluster";
import { Input } from "../../input";
import { observable, autorun } from "mobx";
import { observer, disposeOnUnmount } from "mobx-react";
import { SubTitle } from "../../layout/sub-title";
import { isRequired } from "../../input/input_validators";
interface Props {
cluster: Cluster;
}
@observer
export class ClusterNameSetting extends React.Component<Props> {
@observable name = "";
componentDidMount() {
disposeOnUnmount(this,
autorun(() => {
this.name = this.props.cluster.preferences.clusterName;
})
);
}
save = () => {
this.props.cluster.preferences.clusterName = this.name;
};
onChange = (value: string) => {
this.name = value;
};
render() {
return (
<>
<SubTitle title="Cluster Name" />
<Input
theme="round-black"
validators={isRequired}
value={this.name}
onChange={this.onChange}
onBlur={this.save}
/>
</>
);
}
}
@@ -0,0 +1,118 @@
import React from "react";
import { observer, disposeOnUnmount } 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, autorun } from "mobx";
const options: SelectOption<string>[] = [
{ value: "", label: "Auto detect" },
...prometheusProviders.map(pp => ({value: pp.id, label: pp.name}))
];
interface Props {
cluster: Cluster;
}
@observer
export class ClusterPrometheusSetting extends React.Component<Props> {
@observable path = "";
@observable provider = "";
@computed get canEditPrometheusPath() {
if (this.provider === "" || this.provider === "lens") return false;
return true;
}
componentDidMount() {
disposeOnUnmount(this,
autorun(() => {
const { prometheus, prometheusProvider } = this.props.cluster.preferences;
if (prometheus) {
const prefix = prometheus.prefix || "";
this.path = `${prometheus.namespace}/${prometheus.service}:${prometheus.port}${prefix}`;
} else {
this.path = "";
}
if (prometheusProvider) {
this.provider = prometheusProvider.type;
} else {
this.provider = "";
}
})
);
}
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 (
<>
<SubTitle title="Prometheus installation method"/>
<p>
Use pre-installed Prometheus service for metrics. Please refer to the{" "}
<a href="https://github.com/lensapp/lens/blob/master/troubleshooting/custom-prometheus.md" target="_blank" rel="noreferrer">guide</a>{" "}
for possible configuration changes.
</p>
<Select
value={this.provider}
onChange={({value}) => {
this.provider = value;
this.onSaveProvider();
}}
options={options}
/>
<small className="hint">What query format is used to fetch metrics from Prometheus</small>
{this.canEditPrometheusPath && (
<>
<p>Prometheus service address.</p>
<Input
theme="round-black"
value={this.path}
onChange={(value) => this.path = value}
onBlur={this.onSavePath}
placeholder="<namespace>/<service>:<port>"
/>
<small className="hint">
An address to an existing Prometheus installation{" "}
({"<namespace>/<service>:<port>"}). Lens tries to auto-detect address if left empty.
</small>
</>
)}
</>
);
}
}
@@ -0,0 +1,50 @@
import React from "react";
import { observable, autorun } from "mobx";
import { observer, disposeOnUnmount } from "mobx-react";
import { Cluster } from "../../../../main/cluster";
import { Input, InputValidators } from "../../input";
import { SubTitle } from "../../layout/sub-title";
interface Props {
cluster: Cluster;
}
@observer
export class ClusterProxySetting extends React.Component<Props> {
@observable proxy = "";
componentDidMount() {
disposeOnUnmount(this,
autorun(() => {
this.proxy = this.props.cluster.preferences.httpsProxy || "";
})
);
}
save = () => {
this.props.cluster.preferences.httpsProxy = this.proxy;
};
onChange = (value: string) => {
this.proxy = value;
};
render() {
return (
<>
<SubTitle title="HTTP Proxy" />
<Input
theme="round-black"
value={this.proxy}
onChange={this.onChange}
onBlur={this.save}
placeholder="http://<address>:<port>"
validators={this.proxy ? InputValidators.isUrl : undefined}
/>
<small className="hint">
HTTP Proxy server. Used for communicating with Kubernetes API.
</small>
</>
);
}
}
@@ -0,0 +1,38 @@
import React from "react";
import { observer } from "mobx-react";
import { clusterStore } from "../../../../common/cluster-store";
import { Cluster } from "../../../../main/cluster";
import { autobind } from "../../../utils";
import { Button } from "../../button";
import { ConfirmDialog } from "../../confirm-dialog";
interface Props {
cluster: Cluster;
}
@observer
export class RemoveClusterButton extends React.Component<Props> {
@autobind()
confirmRemoveCluster() {
const { cluster } = this.props;
ConfirmDialog.open({
message: <p>Are you sure you want to remove <b>{cluster.preferences.clusterName}</b> from Lens?</p>,
labelOk: "Yes",
labelCancel: "No",
ok: async () => {
await clusterStore.removeById(cluster.id);
}
});
}
render() {
const { cluster } = this.props;
return (
<Button accent onClick={this.confirmRemoveCluster} className="button-area" disabled={cluster.isManaged}>
Remove Cluster
</Button>
);
}
}
@@ -0,0 +1,63 @@
import "./cluster-metrics-setting.scss";
import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
import { Cluster } from "../../../../main/cluster";
import { observable, reaction } from "mobx";
import { Badge } from "../../badge/badge";
import { Icon } from "../../icon/icon";
interface Props {
cluster: Cluster;
}
@observer
export class ShowMetricsSetting extends React.Component<Props> {
@observable hiddenMetrics = observable.set<string>();
componentDidMount() {
this.hiddenMetrics = observable.set<string>(this.props.cluster.preferences.hiddenMetrics ?? []);
disposeOnUnmount(this, [
reaction(() => this.props.cluster.preferences.hiddenMetrics, () => {
this.hiddenMetrics = observable.set<string>(this.props.cluster.preferences.hiddenMetrics ?? []);
}),
]);
}
removeMetric(metric: string) {
this.hiddenMetrics.delete(metric);
this.props.cluster.preferences.hiddenMetrics = Array.from(this.hiddenMetrics);
}
renderMetrics() {
return (
Array.from(this.hiddenMetrics).map(name => {
const tooltipId = `${name}`;
return (
<Badge key={name}>
<span id={tooltipId}>{name}</span>
<Icon
smallest
material="clear"
onClick={() => this.removeMetric(name)}
tooltip="Remove"
/>
</Badge>
);
})
);
}
render() {
return (
<div className="MetricsSelect flex wrap gaps">
{this.renderMetrics()}
</div>
);
}
}
@@ -0,0 +1,2 @@
export * from "./cluster-settings";
export * from "./cluster-settings.command";