1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Cleaning settings page view (#3156)

* Making inputs consistent

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fixing hover effect in inputs

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding separators to sidebar menu

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fine-tuning general section

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fine-tuning hidden metrics area

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* EntityIcon in Settings sidebar

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Moving cluster icon settings on top

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Shrink Apply button a big

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2021-06-23 11:27:26 +03:00 committed by GitHub
parent 1cc5607987
commit 7739b387cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 159 additions and 100 deletions

View File

@ -281,7 +281,9 @@ export class MetricsSettings extends React.Component<Props> {
waiting={this.inProgress} waiting={this.inProgress}
onClick={() => this.save()} onClick={() => this.save()}
primary primary
disabled={!this.changed} /> disabled={!this.changed}
className="w-60 h-14"
/>
{this.canUpgrade && (<small className="hint"> {this.canUpgrade && (<small className="hint">
An update is available for enabled metrics components. An update is available for enabled metrics components.

View File

@ -33,6 +33,7 @@ import { EntitySettingRegistry } from "../../../extensions/registries";
import type { EntitySettingsRouteParams } from "../../../common/routes"; import type { EntitySettingsRouteParams } from "../../../common/routes";
import { groupBy } from "lodash"; import { groupBy } from "lodash";
import { SettingLayout } from "../layout/setting-layout"; import { SettingLayout } from "../layout/setting-layout";
import { HotbarIcon } from "../hotbar/hotbar-icon";
interface Props extends RouteComponentProps<EntitySettingsRouteParams> { interface Props extends RouteComponentProps<EntitySettingsRouteParams> {
} }
@ -83,10 +84,19 @@ export class EntitySettings extends React.Component<Props> {
return ( return (
<> <>
<h2>{this.entity.metadata.name}</h2> <div className="flex items-center pb-8">
<HotbarIcon
uid={this.entity.metadata.uid}
title={this.entity.metadata.name}
source={this.entity.metadata.source}
src={this.entity.spec.icon?.src}
/>
<h2>{this.entity.metadata.name}</h2>
</div>
<Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}> <Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}>
{ groups.map((group, groupIndex) => ( { groups.map((group, groupIndex) => (
<React.Fragment key={`group-${groupIndex}`}> <React.Fragment key={`group-${groupIndex}`}>
<hr/>
<div className="header">{group[0]}</div> <div className="header">{group[0]}</div>
{ group[1].map((setting, index) => ( { group[1].map((setting, index) => (
<Tab <Tab

View File

@ -45,6 +45,7 @@ import { Install } from "./install";
import { InstalledExtensions } from "./installed-extensions"; import { InstalledExtensions } from "./installed-extensions";
import { Notice } from "./notice"; import { Notice } from "./notice";
import { SettingLayout } from "../layout/setting-layout"; import { SettingLayout } from "../layout/setting-layout";
import { docsUrl } from "../../../common/vars";
function getMessageFromError(error: any): string { function getMessageFromError(error: any): string {
if (!error || typeof error !== "object") { if (!error || typeof error !== "object") {
@ -514,7 +515,13 @@ export class Extensions extends React.Component<Props> {
<section> <section>
<h1>Extensions</h1> <h1>Extensions</h1>
<Notice/> <Notice>
<p>
Add new features via Lens Extensions.{" "}
Check out <a href={`${docsUrl}/extensions/`} target="_blank" rel="noreferrer">docs</a>{" "}
and list of <a href="https://github.com/lensapp/lens-extensions/blob/main/README.md" target="_blank" rel="noreferrer">available extensions</a>.
</p>
</Notice>
<Install <Install
supportedFormats={supportedFormats} supportedFormats={supportedFormats}

View File

@ -20,17 +20,14 @@
*/ */
import styles from "./notice.module.css"; import styles from "./notice.module.css";
import React from "react"; import React, { DOMAttributes } from "react";
import { docsUrl } from "../../../common/vars";
export function Notice() { interface Props extends DOMAttributes<any> {}
export function Notice(props: Props) {
return ( return (
<div className={styles.notice}> <div className={styles.notice}>
<p> {props.children}
Add new features via Lens Extensions.{" "}
Check out <a href={`${docsUrl}/extensions/`} target="_blank" rel="noreferrer">docs</a>{" "}
and list of <a href="https://github.com/lensapp/lens-extensions/blob/main/README.md" target="_blank" rel="noreferrer">available extensions</a>.
</p>
</div> </div>
); );
} }

View File

@ -40,12 +40,16 @@ export function GeneralSettings({ entity }: EntitySettingViewProps) {
return ( return (
<section> <section>
<section> <section>
<components.ClusterNameSetting cluster={cluster} /> <div className="flex">
<div className="flex-grow pr-8">
<components.ClusterNameSetting cluster={cluster} />
</div>
<div>
<components.ClusterIconSetting cluster={cluster} entity={entity as KubernetesCluster} />
</div>
</div>
</section> </section>
<section> <section className="small">
<components.ClusterIconSetting cluster={cluster} entity={entity as KubernetesCluster} />
</section>
<section>
<components.ClusterKubeconfig cluster={cluster} /> <components.ClusterKubeconfig cluster={cluster} />
</section> </section>
</section> </section>
@ -106,10 +110,9 @@ export function MetricsSettings({ entity }: EntitySettingViewProps) {
<section> <section>
<components.ClusterPrometheusSetting cluster={cluster} /> <components.ClusterPrometheusSetting cluster={cluster} />
</section> </section>
<hr/>
<section> <section>
<components.ClusterMetricsSetting cluster={cluster} /> <components.ClusterMetricsSetting cluster={cluster} />
</section>
<section>
<components.ShowMetricsSetting cluster={cluster} /> <components.ShowMetricsSetting cluster={cluster} />
</section> </section>
</section> </section>

View File

@ -54,6 +54,7 @@ export class ClusterAccessibleNamespaces extends React.Component<Props> {
this.namespaces.delete(oldNamesapce); this.namespaces.delete(oldNamesapce);
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces); this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
}} }}
inputTheme="round-black"
/> />
<small className="hint"> <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. 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.

View File

@ -21,15 +21,13 @@
import React from "react"; import React from "react";
import type { Cluster } from "../../../../main/cluster"; import type { Cluster } from "../../../../main/cluster";
//import { FilePicker, OverSizeLimitStyle } from "../../file-picker";
import { boundMethod } from "../../../utils"; import { boundMethod } from "../../../utils";
import { Button } from "../../button";
import { observable } from "mobx"; import { observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { SubTitle } from "../../layout/sub-title";
import { HotbarIcon } from "../../hotbar/hotbar-icon"; import { HotbarIcon } from "../../hotbar/hotbar-icon";
import type { KubernetesCluster } from "../../../../common/catalog-entities"; import type { KubernetesCluster } from "../../../../common/catalog-entities";
import { FilePicker, OverSizeLimitStyle } from "../../file-picker"; import { FilePicker, OverSizeLimitStyle } from "../../file-picker";
import { MenuActions, MenuItem } from "../../menu";
enum GeneralInputStatus { enum GeneralInputStatus {
CLEAN = "clean", CLEAN = "clean",
@ -46,6 +44,8 @@ export class ClusterIconSetting extends React.Component<Props> {
@observable status = GeneralInputStatus.CLEAN; @observable status = GeneralInputStatus.CLEAN;
@observable errorText?: string; @observable errorText?: string;
private element = React.createRef<HTMLDivElement>();
@boundMethod @boundMethod
async onIconPick([file]: File[]) { async onIconPick([file]: File[]) {
const { cluster } = this.props; const { cluster } = this.props;
@ -66,16 +66,11 @@ export class ClusterIconSetting extends React.Component<Props> {
} }
} }
getClearButton() { @boundMethod
if (this.props.cluster.preferences.icon) { onUploadClick() {
return <Button const input = this.element.current.querySelector("input[type=file]") as HTMLInputElement;
label="Clear"
tooltip="Revert back to default icon"
onClick={() => this.onIconPick([])}
/>;
}
return null; input.click();
} }
render() { render() {
@ -87,24 +82,32 @@ export class ClusterIconSetting extends React.Component<Props> {
title={entity.metadata.name} title={entity.metadata.name}
source={entity.metadata.source} source={entity.metadata.source}
src={entity.spec.icon?.src} src={entity.spec.icon?.src}
size={53}
/> />
<span style={{marginRight: "var(--unit)"}}>Browse for new icon...</span>
</> </>
); );
return ( return (
<> <div ref={this.element}>
<SubTitle title="Cluster Icon" /> <div className="file-loader flex flex-row items-center">
<div className="file-loader"> <div className="mr-5">
<FilePicker <FilePicker
accept="image/*" accept="image/*"
label={label} label={label}
onOverSizeLimit={OverSizeLimitStyle.FILTER} onOverSizeLimit={OverSizeLimitStyle.FILTER}
handler={this.onIconPick} handler={this.onIconPick}
/> />
{this.getClearButton()} </div>
<MenuActions toolbar={false} autoCloseOnSelect={true} triggerIcon={{ material: "more_horiz" }}>
<MenuItem onClick={this.onUploadClick}>
Upload Icon
</MenuItem>
<MenuItem onClick={() => this.onIconPick([])} disabled={!this.props.cluster.preferences.icon}>
Clear
</MenuItem>
</MenuActions>
</div> </div>
</> </div>
); );
} }
} }

View File

@ -25,6 +25,7 @@ import { observer } from "mobx-react";
import { SubTitle } from "../../layout/sub-title"; import { SubTitle } from "../../layout/sub-title";
import { boundMethod } from "../../../../common/utils"; import { boundMethod } from "../../../../common/utils";
import { shell } from "electron"; import { shell } from "electron";
import { Notice } from "../../+extensions/notice";
interface Props { interface Props {
cluster: Cluster; cluster: Cluster;
@ -42,14 +43,12 @@ export class ClusterKubeconfig extends React.Component<Props> {
render() { render() {
return ( return (
<> <Notice>
<SubTitle title="Kubeconfig" /> <SubTitle title="Kubeconfig" />
<span> <span>
<a className="link value" onClick={this.openKubeconfig}>{this.props.cluster.kubeConfigPath}</a> <a className="link value" onClick={this.openKubeconfig}>{this.props.cluster.kubeConfigPath}</a>
</span> </span>
</Notice>
</>
); );
} }
} }

View File

@ -100,6 +100,7 @@ export class ClusterMetricsSetting extends React.Component<Props> {
options={Object.values(ClusterMetricsResourceType)} options={Object.values(ClusterMetricsResourceType)}
onChange={this.onChangeSelect} onChange={this.onChangeSelect}
formatOptionLabel={this.formatOptionLabel} formatOptionLabel={this.formatOptionLabel}
themeName="lens"
/> />
<Button <Button
primary primary
@ -118,7 +119,7 @@ export class ClusterMetricsSetting extends React.Component<Props> {
render() { render() {
return ( return (
<div className="MetricsSelect"> <div className="MetricsSelec0 mb-5">
<SubTitle title={"Hide metrics from the UI"}/> <SubTitle title={"Hide metrics from the UI"}/>
<div className="flex gaps"> <div className="flex gaps">
{this.renderMetricsSelect()} {this.renderMetricsSelect()}

View File

@ -138,26 +138,30 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
this.onSaveProvider(); this.onSaveProvider();
}} }}
options={this.options} options={this.options}
themeName="lens"
/> />
<small className="hint">What query format is used to fetch metrics from Prometheus</small> <small className="hint">What query format is used to fetch metrics from Prometheus</small>
</> </>
} }
</section> </section>
{this.canEditPrometheusPath && ( {this.canEditPrometheusPath && (
<section> <>
<p>Prometheus service address.</p> <hr/>
<Input <section>
theme="round-black" <SubTitle title="Prometheus service address" />
value={this.path} <Input
onChange={(value) => this.path = value} theme="round-black"
onBlur={this.onSavePath} value={this.path}
placeholder="<namespace>/<service>:<port>" onChange={(value) => this.path = value}
/> onBlur={this.onSavePath}
<small className="hint"> placeholder="<namespace>/<service>:<port>"
An address to an existing Prometheus installation{" "} />
({"<namespace>/<service>:<port>"}). {productName} tries to auto-detect address if left empty. <small className="hint">
</small> An address to an existing Prometheus installation{" "}
</section> ({"<namespace>/<service>:<port>"}). {productName} tries to auto-detect address if left empty.
</small>
</section>
</>
)} )}
</> </>
); );

View File

@ -25,6 +25,7 @@ import type { Cluster } from "../../../../main/cluster";
import { observable, reaction, makeObservable } from "mobx"; import { observable, reaction, makeObservable } from "mobx";
import { Badge } from "../../badge/badge"; import { Badge } from "../../badge/badge";
import { Icon } from "../../icon/icon"; import { Icon } from "../../icon/icon";
import { Notice } from "../../+extensions/notice";
interface Props { interface Props {
cluster: Cluster; cluster: Cluster;
@ -55,14 +56,20 @@ export class ShowMetricsSetting extends React.Component<Props> {
} }
renderMetrics() { renderMetrics() {
const metrics = Array.from(this.hiddenMetrics);
if (!metrics.length) {
return (
<div className="flex-grow text-center">All metrics are visible on the UI</div>
);
}
return ( return (
metrics.map(name => {
Array.from(this.hiddenMetrics).map(name => {
const tooltipId = `${name}`; const tooltipId = `${name}`;
return ( return (
<Badge key={name}> <Badge key={name} flat>
<span id={tooltipId}>{name}</span> <span id={tooltipId}>{name}</span>
<Icon <Icon
smallest smallest
@ -79,9 +86,11 @@ export class ShowMetricsSetting extends React.Component<Props> {
render() { render() {
return ( return (
<div className="MetricsSelect flex wrap gaps"> <Notice>
{this.renderMetrics()} <div className="MetricsSelect flex wrap gaps leading-relaxed">
</div> {this.renderMetrics()}
</div>
</Notice>
); );
} }
} }

View File

@ -23,7 +23,10 @@
.el-contents { .el-contents {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: $padding 0px;
&:not(:empty) {
margin: $padding 0px;
}
.el-value-remove { .el-value-remove {
.Icon { .Icon {

View File

@ -25,7 +25,7 @@ import { observer } from "mobx-react";
import React from "react"; import React from "react";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Input } from "../input"; import { Input, InputProps } from "../input";
import { boundMethod } from "../../utils"; import { boundMethod } from "../../utils";
export interface Props<T> { export interface Props<T> {
@ -37,11 +37,13 @@ export interface Props<T> {
// An optional prop used to convert T to a displayable string // An optional prop used to convert T to a displayable string
// defaults to `String` // defaults to `String`
renderItem?: (item: T, index: number) => React.ReactNode, renderItem?: (item: T, index: number) => React.ReactNode,
inputTheme?: InputProps["theme"];
} }
const defaultProps: Partial<Props<any>> = { const defaultProps: Partial<Props<any>> = {
placeholder: "Add new item...", placeholder: "Add new item...",
renderItem: (item: any, index: number) => <React.Fragment key={index}>{item}</React.Fragment> renderItem: (item: any, index: number) => <React.Fragment key={index}>{item}</React.Fragment>,
inputTheme: "round"
}; };
@observer @observer
@ -59,13 +61,13 @@ export class EditableList<T> extends React.Component<Props<T>> {
} }
render() { render() {
const { items, remove, renderItem, placeholder } = this.props; const { items, remove, renderItem, placeholder, inputTheme } = this.props;
return ( return (
<div className="EditableList"> <div className="EditableList">
<div className="el-header"> <div className="el-header">
<Input <Input
theme="round" theme={inputTheme}
onSubmit={this.onSubmit} onSubmit={this.onSubmit}
placeholder={placeholder} placeholder={placeholder}
/> />

View File

@ -227,12 +227,12 @@ export class FilePicker extends React.Component<Props> {
getIconRight(): React.ReactNode { getIconRight(): React.ReactNode {
switch (this.status) { switch (this.status) {
case FileInputStatus.CLEAR:
return <Icon className="clean" material="cloud_upload"></Icon>;
case FileInputStatus.PROCESSING: case FileInputStatus.PROCESSING:
return <Spinner />; return <Spinner />;
case FileInputStatus.ERROR: case FileInputStatus.ERROR:
return <Icon material="error" title={this.errorText}></Icon>; return <Icon material="error" title={this.errorText}></Icon>;
default:
return null;
} }
} }
} }

View File

@ -136,10 +136,15 @@
border-color: var(--inputControlBorder); border-color: var(--inputControlBorder);
color: var(--textColorTertiary); color: var(--textColorTertiary);
padding: $padding; padding: $padding;
transition: border-color 0.1s;
&:hover { &:hover {
border-color: var(--inputControlHoverBorder); border-color: var(--inputControlHoverBorder);
} }
&:focus-within {
border-color: $colorInfo;
}
} }
} }
} }

View File

@ -20,17 +20,19 @@
*/ */
.SettingLayout { .SettingLayout {
--width: 75%;
--nav-width: 180px;
--nav-column-width: 30vw; --nav-column-width: 30vw;
width: 100%; width: 100%;
height: 100%; height: 100%;
display: grid !important; display: grid;
color: var(--settingsColor); color: var(--settingsColor);
position: fixed;
@include media("<1000px") { z-index: 13!important;
--width: 85%; left: 0;
} top: 0;
right: 0;
bottom: 0;
height: unset;
background-color: var(--settingsBackground);
&.showNavigation { &.showNavigation {
grid-template-columns: var(--nav-column-width) 1fr; grid-template-columns: var(--nav-column-width) 1fr;
@ -40,18 +42,6 @@
} }
} }
// covers whole app view area
&.showOnTop {
position: fixed !important; // allow to cover ClustersMenu
z-index: 13;
left: 0;
top: 0;
right: 0;
bottom: 0;
height: unset;
background-color: var(--settingsBackground);
}
> .sidebarRegion { > .sidebarRegion {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@ -63,12 +53,25 @@
padding: 60px 0 60px 20px; padding: 60px 0 60px 20px;
h2 { h2 {
margin-bottom: 10px; font-size: 15px;
font-size: 18px;
padding: 6px 10px;
overflow-wrap: anywhere; overflow-wrap: anywhere;
color: var(--textColorAccent); color: var(--textColorAccent);
font-weight: 600; font-weight: 600;
padding-right: 20px;
word-break: break-word;
}
hr {
margin-top: 10px;
margin-bottom: 10px;
margin-left: 10px;
margin-right: 20px;
height: 1px;
border-top: thin solid var(--hrColor);
&:first-child {
display: none;
}
} }
.Tabs { .Tabs {
@ -78,6 +81,7 @@
font-weight: 800; font-weight: 800;
line-height: 16px; line-height: 16px;
text-transform: uppercase; text-transform: uppercase;
color: var(--textColorPrimary);
&:first-child { &:first-child {
padding-top: 0; padding-top: 0;
@ -112,9 +116,14 @@
> .label { > .label {
width: 100%; width: 100%;
font-weight: 500;
} }
} }
} }
.HotbarIcon {
margin: 0 11px;
}
} }
} }
@ -124,7 +133,9 @@
justify-content: center; justify-content: center;
> .content { > .content {
width: var(--width); width: 100%;
max-width: 740px;
min-width: 460px;
padding: 60px 40px 80px; padding: 60px 40px 80px;
> section { > section {
@ -157,7 +168,7 @@
} }
.Icon { .Icon {
color: var(--textColorSecondary); color: var(--textColorTertiary);
} }
} }
@ -200,7 +211,7 @@
font-size: 18px; font-size: 18px;
line-height: 20px; line-height: 20px;
font-weight: 600; font-weight: 600;
margin-bottom: 20px; margin-bottom: 30px;
} }
a { a {
@ -210,6 +221,7 @@
.hint { .hint {
margin-top: 8px; margin-top: 8px;
font-size: 14px; font-size: 14px;
line-height: 20px;
} }
.SubTitle { .SubTitle {

View File

@ -81,7 +81,7 @@ export class SettingLayout extends React.Component<SettingLayoutProps> {
contentClass, provideBackButtonNavigation, contentClass, provideBackButtonNavigation,
contentGaps, navigation, children, ...elemProps contentGaps, navigation, children, ...elemProps
} = this.props; } = this.props;
const className = cssNames("SettingLayout", "showOnTop", { showNavigation: navigation }, this.props.className); const className = cssNames("SettingLayout", { showNavigation: navigation }, this.props.className);
return ( return (
<div {...elemProps} className={className}> <div {...elemProps} className={className}>

View File

@ -213,6 +213,7 @@ html {
:hover { :hover {
&.Select__control { &.Select__control {
box-shadow: 0 0 0 1px var(--inputControlHoverBorder); box-shadow: 0 0 0 1px var(--inputControlHoverBorder);
transition: box-shadow 0.1s;
} }
} }