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

Polishing Kubernetes section view in Preferences (#3603)

* Using RemovableItem for items in Kubernetes page

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

* Helm Charts empty state

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

* Empty state for kubeconfig sync list

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

* Remove unused styles

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

* Fixing tests

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2021-08-16 16:52:14 +03:00 committed by GitHub
parent e979cbf7c7
commit fa58b94cd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 156 additions and 125 deletions

View File

@ -73,7 +73,7 @@ describe("Lens integration tests", () => {
} }
await app.client.click("[data-testid=kubernetes-tab]"); await app.client.click("[data-testid=kubernetes-tab]");
await app.client.waitUntilTextExists("div.repos .repoName", repos[0].name); // wait for the helm-cli to fetch the repo(s) await app.client.waitUntilTextExists("[data-testid=repository-name]", repos[0].name); // wait for the helm-cli to fetch the repo(s)
await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down
await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text) await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text)
}); });

View File

@ -515,7 +515,7 @@ export class Extensions extends React.Component<Props> {
<section> <section>
<h1>Extensions</h1> <h1>Extensions</h1>
<Notice> <Notice className="mb-14 mt-3">
<p> <p>
Add new features via Lens Extensions.{" "} Add new features via Lens Extensions.{" "}
Check out <a href={`${docsUrl}/extensions/`} target="_blank" rel="noreferrer">docs</a>{" "} Check out <a href={`${docsUrl}/extensions/`} target="_blank" rel="noreferrer">docs</a>{" "}

View File

@ -20,7 +20,7 @@
*/ */
.notice { .notice {
@apply p-8 flex flex-col gap-2 mb-14 mt-3 rounded-lg; @apply p-8 flex flex-col gap-2 rounded-lg;
background-color: var(--inputControlBackground); background-color: var(--inputControlBackground);
border: 1px solid var(--boxShadow); border: 1px solid var(--boxShadow);
color: var(--textColorTertiary); color: var(--textColorTertiary);

View File

@ -21,12 +21,15 @@
import styles from "./notice.module.css"; import styles from "./notice.module.css";
import React, { DOMAttributes } from "react"; import React, { DOMAttributes } from "react";
import { cssNames } from "../../utils";
interface Props extends DOMAttributes<any> {} interface Props extends DOMAttributes<any> {
className?: string;
}
export function Notice(props: Props) { export function Notice(props: Props) {
return ( return (
<div className={styles.notice}> <div className={cssNames(styles.notice, props.className)}>
{props.children} {props.children}
</div> </div>
); );

View File

@ -19,24 +19,15 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
.HelmCharts { .repos {
.repos { @apply mt-6 flex flex-col;
margin-top: 20px; }
.repo { .repoName {
background: var(--inputControlBackground);
border-radius: 4px;
padding: 12px 16px;
box-shadow: 0 0 0 1px var(--secondaryBackground);
.repoName {
font-weight: 500; font-weight: 500;
margin-bottom: 8px; margin-bottom: 8px;
} }
.repoUrl { .repoUrl {
color: var(--textColorDimmed); color: var(--textColorDimmed);
}
}
}
} }

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import "./helm-charts.scss"; import styles from "./helm-charts.module.css";
import React from "react"; import React from "react";
import { action, computed, observable, makeObservable } from "mobx"; import { action, computed, observable, makeObservable } from "mobx";
@ -31,6 +31,9 @@ import { Notifications } from "../notifications";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { AddHelmRepoDialog } from "./add-helm-repo-dialog"; import { AddHelmRepoDialog } from "./add-helm-repo-dialog";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { RemovableItem } from "./removable-item";
import { Notice } from "../+extensions/notice";
import { Spinner } from "../spinner";
@observer @observer
export class HelmCharts extends React.Component { export class HelmCharts extends React.Component {
@ -117,9 +120,36 @@ export class HelmCharts extends React.Component {
); );
}; };
renderRepositories() {
const repos = Array.from(this.addedRepos);
if (this.loading) {
return <div className="pt-5 relative"><Spinner center/></div>;
}
if (!repos.length) {
return (
<Notice>
<div className="flex-grow text-center">The repositories have not been added yet</div>
</Notice>
);
}
return repos.map(([name, repo]) => {
return (
<RemovableItem key={name} onRemove={() => this.removeRepo(repo)} className="mt-3">
<div>
<div data-testid="repository-name" className={styles.repoName}>{name}</div>
<div className={styles.repoUrl}>{repo.url}</div>
</div>
</RemovableItem>
);
});
}
render() { render() {
return ( return (
<div className="HelmCharts"> <div>
<div className="flex gaps"> <div className="flex gaps">
<Select id="HelmRepoSelect" <Select id="HelmRepoSelect"
placeholder="Repositories" placeholder="Repositories"
@ -139,22 +169,8 @@ export class HelmCharts extends React.Component {
/> />
</div> </div>
<AddHelmRepoDialog onAddRepo={() => this.loadRepos()}/> <AddHelmRepoDialog onAddRepo={() => this.loadRepos()}/>
<div className="repos flex gaps column"> <div className={styles.repos}>
{Array.from(this.addedRepos).map(([name, repo]) => { {this.renderRepositories()}
return (
<div key={name} className="repo flex gaps align-center justify-space-between">
<div>
<div className="repoName">{name}</div>
<div className="repoUrl">{repo.url}</div>
</div>
<Icon
material="delete"
onClick={() => this.removeRepo(repo)}
tooltip="Remove"
/>
</div>
);
})}
</div> </div>
</div> </div>
); );

View File

@ -18,19 +18,20 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import React from "react";
import { Avatar, IconButton, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText, Paper } from "@material-ui/core";
import { Description, Folder, Delete, HelpOutline } from "@material-ui/icons";
import { action, computed, observable, reaction, makeObservable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import fse from "fs-extra"; import fse from "fs-extra";
import { action, computed, makeObservable, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import { Notice } from "../+extensions/notice";
import { KubeconfigSyncEntry, KubeconfigSyncValue, UserStore } from "../../../common/user-store"; import { KubeconfigSyncEntry, KubeconfigSyncValue, UserStore } from "../../../common/user-store";
import { Spinner } from "../spinner"; import { isWindows } from "../../../common/vars";
import logger from "../../../main/logger"; import logger from "../../../main/logger";
import { iter, multiSet } from "../../utils"; import { iter, multiSet } from "../../utils";
import { isWindows } from "../../../common/vars"; import { SubTitle } from "../layout/sub-title";
import { PathPicker } from "../path-picker/path-picker"; import { PathPicker } from "../path-picker/path-picker";
import { Spinner } from "../spinner";
import { RemovableItem } from "./removable-item";
interface SyncInfo { interface SyncInfo {
type: "file" | "folder" | "unknown"; type: "file" | "folder" | "unknown";
@ -111,41 +112,29 @@ export class KubeconfigSyncs extends React.Component {
@action @action
onPick = async (filePaths: string[]) => multiSet(this.syncs, await getAllEntries(filePaths)); onPick = async (filePaths: string[]) => multiSet(this.syncs, await getAllEntries(filePaths));
renderEntryIcon(entry: Entry) { getIconName(entry: Entry) {
switch (entry.info.type) { switch (entry.info.type) {
case "file": case "file":
return <Description />; return "description";
case "folder": case "folder":
return <Folder />; return "folder";
case "unknown": case "unknown":
return <HelpOutline />; return "help_outline";
} }
} }
renderEntry = (entry: Entry) => { renderEntry = (entry: Entry) => {
return ( return (
<Paper className="entry" key={entry.filePath} elevation={3}> <RemovableItem
<ListItem> key={entry.filePath}
<ListItemAvatar> onRemove={() => this.syncs.delete(entry.filePath)}
<Avatar> className="mt-3"
{this.renderEntryIcon(entry)} icon={this.getIconName(entry)}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={entry.filePath}
className="description"
/>
<ListItemSecondaryAction className="action">
<IconButton
edge="end"
aria-label="delete"
onClick={() => this.syncs.delete(entry.filePath)}
> >
<Delete /> <div className="flex-grow break-all">
</IconButton> {entry.filePath}
</ListItemSecondaryAction> </div>
</ListItem> </RemovableItem>
</Paper>
); );
}; };
@ -160,27 +149,34 @@ export class KubeconfigSyncs extends React.Component {
); );
} }
if (!entries.length) {
return ( return (
<List className="kubeconfig-sync-list"> <Notice className="mt-3">
<div className="flex-grow text-center">No files and folders have been synced yet</div>
</Notice>
);
}
return (
<div>
{entries.map(this.renderEntry)} {entries.map(this.renderEntry)}
</List> </div>
); );
} }
renderSyncButtons() { renderSyncButtons() {
if (isWindows) { if (isWindows) {
return ( return (
<div className="flex gaps align-center"> <div className="flex gaps align-center mb-5">
<PathPicker <PathPicker
label="Sync file(s)" label="Sync file(s)"
className="box grow"
onPick={this.onPick} onPick={this.onPick}
buttonLabel="Sync" buttonLabel="Sync"
properties={["showHiddenFiles", "multiSelections", "openFile"]} properties={["showHiddenFiles", "multiSelections", "openFile"]}
/> />
<span>or</span>
<PathPicker <PathPicker
label="Sync folder(s)" label="Sync folder(s)"
className="box grow"
onPick={this.onPick} onPick={this.onPick}
buttonLabel="Sync" buttonLabel="Sync"
properties={["showHiddenFiles", "multiSelections", "openDirectory"]} properties={["showHiddenFiles", "multiSelections", "openDirectory"]}
@ -190,12 +186,14 @@ export class KubeconfigSyncs extends React.Component {
} }
return ( return (
<div className="self-start mb-5">
<PathPicker <PathPicker
label="Sync file(s) and folder(s)" label="Sync Files and Folders"
onPick={this.onPick} onPick={this.onPick}
buttonLabel="Sync" buttonLabel="Sync"
properties={["showHiddenFiles", "multiSelections", "openFile", "openDirectory"]} properties={["showHiddenFiles", "multiSelections", "openFile", "openDirectory"]}
/> />
</div>
); );
} }
@ -203,6 +201,7 @@ export class KubeconfigSyncs extends React.Component {
return ( return (
<> <>
{this.renderSyncButtons()} {this.renderSyncButtons()}
<SubTitle title="Synced Items" className="pt-5"/>
{this.renderEntries()} {this.renderEntries()}
</> </>
); );

View File

@ -46,7 +46,7 @@ export const KubectlBinaries = observer(() => {
return ( return (
<> <>
<section className="small"> <section>
<SubTitle title="Kubectl binary download"/> <SubTitle title="Kubectl binary download"/>
<FormSwitch <FormSwitch
control={ control={
@ -60,9 +60,7 @@ export const KubectlBinaries = observer(() => {
/> />
</section> </section>
<hr className="small"/> <section>
<section className="small">
<SubTitle title="Download mirror" /> <SubTitle title="Download mirror" />
<Select <Select
placeholder="Download mirror for kubectl" placeholder="Download mirror for kubectl"
@ -74,9 +72,7 @@ export const KubectlBinaries = observer(() => {
/> />
</section> </section>
<hr className="small"/> <section>
<section className="small">
<SubTitle title="Directory for binaries" /> <SubTitle title="Directory for binaries" />
<Input <Input
theme="round-black" theme="round-black"
@ -92,9 +88,7 @@ export const KubectlBinaries = observer(() => {
</div> </div>
</section> </section>
<hr className="small"/> <section>
<section className="small">
<SubTitle title="Path to kubectl binary" /> <SubTitle title="Path to kubectl binary" />
<Input <Input
theme="round-black" theme="round-black"

View File

@ -23,31 +23,4 @@
.loading-spinner { .loading-spinner {
margin: auto; margin: auto;
} }
.kubeconfig-sync-list {
.entry {
&.MuiPaper-root {
background-color: var(--inputControlBackground);
margin-bottom: var(--flex-gap, 1em);
color: inherit;
}
.MuiAvatar-root {
color: var(--buttonPrimaryBackground);
font-size: calc(2.5 * var(--unit));
}
.description {
font-family: monospace;
.MuiTypography-body1 {
font-size: var(--font-size);
}
}
.action .MuiIconButton-root {
font-size: calc(2.5 * var(--unit));
}
}
}
} }

View File

@ -0,0 +1,7 @@
.item {
--flex-gap: 1.2rem;
@apply rounded-md px-7 py-5 shadow-sm;
background: var(--inputControlBackground);
min-height: 65px;
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import styles from "./removable-item.module.css";
import React, { DOMAttributes } from "react";
import { cssNames } from "../../utils";
import { Icon } from "../icon";
interface Props extends DOMAttributes<any>{
icon?: string;
onRemove: () => void;
className?: string;
}
export function RemovableItem({icon, onRemove, children, className, ...rest}: Props) {
return (
<div className={cssNames(styles.item, "flex gaps align-center justify-space-between", className)} {...rest}>
{icon && (
<Icon material={icon}/>
)}
{children}
<Icon
material="delete"
onClick={onRemove}
tooltip="Remove"
/>
</div>
);
}

View File

@ -43,7 +43,7 @@ export class ClusterKubeconfig extends React.Component<Props> {
render() { render() {
return ( return (
<Notice> <Notice className="mb-14 mt-3">
<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>

View File

@ -228,7 +228,7 @@
margin-top: 0; margin-top: 0;
margin-bottom: 8px; margin-bottom: 8px;
padding-bottom: 0; padding-bottom: 0;
font-size: 12px; font-size: 13px;
line-height: 1; line-height: 1;
} }