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

Workspace overview layout tweaks (#2302)

* tweaks workspace overview layout

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

* cluster settings on top

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

* header logo for add cluster page

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

* tweak landing page

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

* combine left menu icons

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

* always show bottom status bar

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

* tweak

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

* integration test fixes

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

* change cluster menu

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

* first attempt to fix integration test

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* lint

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* get selectors right for integration test

Signed-off-by: Jim Ehrismann <jehrismann@miranits.com>

Co-authored-by: Jim Ehrismann <jehrismann@mirantis.com>
Co-authored-by: Jim Ehrismann <jehrismann@miranits.com>
This commit is contained in:
Jari Kolehmainen 2021-03-10 15:05:55 +02:00 committed by Jim Ehrismann
parent 37d6636d01
commit 3d3a01e6f1
14 changed files with 135 additions and 159 deletions

View File

@ -25,6 +25,7 @@ describe("Lens cluster pages", () => {
let clusterAdded = false; let clusterAdded = false;
const addCluster = async () => { const addCluster = async () => {
await utils.clickWhatsNew(app); await utils.clickWhatsNew(app);
await utils.clickWelcomeNotification(app);
await addMinikubeCluster(app); await addMinikubeCluster(app);
await waitForMinikubeDashboard(app); await waitForMinikubeDashboard(app);
await app.client.click('a[href="/nodes"]'); await app.client.click('a[href="/nodes"]');
@ -345,7 +346,7 @@ describe("Lens cluster pages", () => {
} }
}); });
it(`shows a logs for a pod`, async () => { it(`shows a log for a pod`, async () => {
expect(clusterAdded).toBe(true); expect(clusterAdded).toBe(true);
// Go to Pods page // Go to Pods page
await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text"); await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text");

View File

@ -39,7 +39,7 @@ export function minikubeReady(testNamespace: string): boolean {
} }
export async function addMinikubeCluster(app: Application) { export async function addMinikubeCluster(app: Application) {
await app.client.click("div.add-cluster"); await app.client.click("button.add-button");
await app.client.waitUntilTextExists("div", "Select kubeconfig file"); await app.client.waitUntilTextExists("div", "Select kubeconfig file");
await app.client.click("div.Select__control"); // show the context drop-down list await app.client.click("div.Select__control"); // show the context drop-down list
await app.client.waitUntilTextExists("div", "minikube"); await app.client.waitUntilTextExists("div", "minikube");

View File

@ -47,7 +47,17 @@ export async function appStart() {
export async function clickWhatsNew(app: Application) { export async function clickWhatsNew(app: Application) {
await app.client.waitUntilTextExists("h1", "What's new?"); await app.client.waitUntilTextExists("h1", "What's new?");
await app.client.click("button.primary"); await app.client.click("button.primary");
await app.client.waitUntilTextExists("h1", "Welcome"); await app.client.waitUntilTextExists("h2", "default");
}
export async function clickWelcomeNotification(app: Application) {
const itemsText = await app.client.$("div.info-panel").getText();
if (itemsText === "0 item") {
// welcome notification should be present, dismiss it
await app.client.waitUntilTextExists("div.message", "Welcome!");
await app.client.click("i.Icon.close");
}
} }
type AsyncPidGetter = () => Promise<number>; type AsyncPidGetter = () => Promise<number>;

View File

@ -352,7 +352,7 @@ export class AddCluster extends React.Component {
return ( return (
<DropFileInput onDropFiles={this.onDropKubeConfig}> <DropFileInput onDropFiles={this.onDropKubeConfig}>
<PageLayout className="AddClusters" header={<h2>Add Clusters</h2>}> <PageLayout className="AddClusters" header={<><Icon svg="logo-lens" big /> <h2>Add Clusters</h2></>} showOnTop={true}>
<h2>Add Clusters from Kubeconfig</h2> <h2>Add Clusters from Kubeconfig</h2>
{this.renderInfo()} {this.renderInfo()}
{this.renderKubeConfigSource()} {this.renderKubeConfigSource()}

View File

@ -59,7 +59,7 @@ export class ClusterSettings extends React.Component<Props> {
); );
return ( return (
<PageLayout className="ClusterSettings" header={header}> <PageLayout className="ClusterSettings" header={header} showOnTop={true}>
<Status cluster={cluster}></Status> <Status cluster={cluster}></Status>
<General cluster={cluster}></General> <General cluster={cluster}></General>
<Features cluster={cluster}></Features> <Features cluster={cluster}></Features>

View File

@ -1,65 +1,15 @@
.LandingPage { .PageLayout.LandingOverview {
width: 100%; --width: 100%;
height: 100%; --height: 100%;
text-align: center; text-align: center;
z-index: 0;
&::after {
content: "";
background: url(../../components/icon/crane.svg) no-repeat;
background-position: 0 35%;
background-size: 85%;
background-clip: content-box;
opacity: .75;
top: 0;
left: 0;
bottom: 0;
right: 0;
position: absolute;
z-index: -1;
.theme-light & {
opacity: 0.2;
}
}
.startup-hint { .content-wrapper {
$bgc: $mainBackground;
$arrowSize: 10px;
position: absolute; .content {
left: 0; margin: unset;
top: 25px; max-width: unset;
margin: $padding;
padding: $padding * 2;
width: 320px;
background: $bgc;
color: $textColorAccent;
filter: drop-shadow(0 0px 2px #ffffff33);
&:before {
content: "";
position: absolute;
width: 0;
height: 0;
border-top: $arrowSize solid transparent;
border-bottom: $arrowSize solid transparent;
border-right: $arrowSize solid $bgc;
right: 100%;
}
.theme-light & {
filter: drop-shadow(0 0px 2px #777);
background: white;
&:before {
border-right-color: white;
}
} }
} }
} }
.PageLayout.LandingOverview {
--width: 80%;
text-align: center;
}

View File

@ -1,11 +1,13 @@
import "./landing-page.scss"; import "./landing-page.scss";
import React from "react"; import React from "react";
import { observable } from "mobx"; import { computed, observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { clusterStore } from "../../../common/cluster-store"; import { clusterStore } from "../../../common/cluster-store";
import { Workspace, workspaceStore } from "../../../common/workspace-store"; import { Workspace, workspaceStore } from "../../../common/workspace-store";
import { WorkspaceOverview } from "./workspace-overview"; import { WorkspaceOverview } from "./workspace-overview";
import { PageLayout } from "../layout/page-layout"; import { PageLayout } from "../layout/page-layout";
import { Notifications } from "../notifications";
import { Icon } from "../icon";
@observer @observer
export class LandingPage extends React.Component { export class LandingPage extends React.Component {
@ -15,42 +17,29 @@ export class LandingPage extends React.Component {
return workspaceStore.currentWorkspace; return workspaceStore.currentWorkspace;
} }
renderStartupHint() { @computed
return ( get clusters() {
<div className="startup-hint flex column gaps" onMouseEnter={() => this.showHint = false}> return clusterStore.getByWorkspaceId(this.workspace.id);
<p>This is the quick launch menu.</p> }
<p>
Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button. componentDidMount() {
</p> const noClustersInScope = !this.clusters.length;
</div> const showStartupHint = this.showHint;
)
if (showStartupHint && noClustersInScope) {
Notifications.info(<><b>Welcome!</b><p>Get started by associating one or more clusters to Lens</p></>, {
timeout: 30_000,
id: "landing-welcome"
});
}
} }
render() { render() {
const clusters = clusterStore.getByWorkspaceId(this.workspace.id); const showBackButton = this.clusters.length > 0;
const noClustersInScope = !clusters.length; const header = <><Icon svg="logo-lens" big /> <h2>{this.workspace.name}</h2></>;
const showStartupHint = this.showHint && noClustersInScope;
const header = <h2>Workspace: {this.workspace.name}</h2>;
if (this.workspace.name === "default" && noClustersInScope) {
return (
<div className="LandingPage flex">
{showStartupHint && this.renderStartupHint()}
<div className="no-clusters flex column gaps box center">
<h1>
Welcome!
</h1>
<p>
Get started by associating one or more clusters to Lens.
</p>
</div>
</div>
)
}
return ( return (
<PageLayout className="LandingOverview flex column gaps" header={header} provideBackButtonNavigation={false} contentGaps={false}> <PageLayout className="LandingOverview flex" header={header} provideBackButtonNavigation={showBackButton} showOnTop={true}>
<WorkspaceOverview workspace={this.workspace}/> <WorkspaceOverview workspace={this.workspace}/>
</PageLayout> </PageLayout>
); );

View File

@ -6,23 +6,35 @@ import { autobind } from "../../utils";
export class ClusterItem implements ItemObject { export class ClusterItem implements ItemObject {
constructor(public cluster: Cluster) {} constructor(public cluster: Cluster) {}
get name() { get name() {
return this.cluster.name; return this.cluster.name;
} }
get distribution() {
return (this.cluster.metadata["distribution"] || "unknown").toString();
}
get version() {
return this.cluster.version;
}
get connectionStatus() {
return this.cluster.online ? "connected" : "disconnected";
}
getName() { getName() {
return this.name; return this.name;
} }
get id() { get id() {
return this.cluster.id; return this.cluster.id;
} }
get clusterId() { get clusterId() {
return this.cluster.id; return this.cluster.id;
} }
getId() { getId() {
return this.id; return this.id;
} }
@ -44,6 +56,7 @@ export class WorkspaceClusterStore extends ItemStore<ClusterItem> {
() => ( () => (
clusterStore clusterStore
.getByWorkspaceId(this.workspaceId) .getByWorkspaceId(this.workspaceId)
.filter(cluster => cluster.enabled)
.map(cluster => new ClusterItem(cluster)) .map(cluster => new ClusterItem(cluster))
) )
); );
@ -56,8 +69,4 @@ export class WorkspaceClusterStore extends ItemStore<ClusterItem> {
return super.removeItem(clusterItem, () => clusterStore.removeById(clusterId)); return super.removeItem(clusterItem, () => clusterStore.removeById(clusterId));
} }
} }
async removeSelectedItems() {
return Promise.all(this.selectedItems.map(this.remove));
}
} }

View File

@ -1,6 +1,8 @@
.WorkspaceOverview { .WorkspaceOverview {
background-color: var(--mainBackground); max-height: 50%;
.Table {
padding-bottom: 60px;
}
.TableCell { .TableCell {
display: flex; display: flex;
align-items: left; align-items: left;
@ -10,5 +12,21 @@
flex-grow: 0.2; flex-grow: 0.2;
padding: 0; padding: 0;
} }
&.connected {
color: var(--colorSuccess);
}
} }
}
.TableCell.status {
flex: 0.1;
}
.TableCell.distribution {
flex: 0.2;
}
.TableCell.version {
flex: 0.2;
}
}

View File

@ -8,6 +8,8 @@ import { ClusterItem, WorkspaceClusterStore } from "./workspace-cluster.store";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { clusterViewURL } from "../cluster-manager/cluster-view.route"; import { clusterViewURL } from "../cluster-manager/cluster-view.route";
import { WorkspaceClusterMenu } from "./workspace-cluster-menu"; import { WorkspaceClusterMenu } from "./workspace-cluster-menu";
import { kebabCase } from "lodash";
import { addClusterURL } from "../+add-cluster";
interface Props { interface Props {
workspace: Workspace; workspace: Workspace;
@ -15,8 +17,9 @@ interface Props {
enum sortBy { enum sortBy {
name = "name", name = "name",
contextName = "contextName", distribution = "distribution",
version = "version", version = "version",
online = "online"
} }
@observer @observer
@ -25,15 +28,15 @@ export class WorkspaceOverview extends Component<Props> {
showCluster = ({ clusterId }: ClusterItem) => { showCluster = ({ clusterId }: ClusterItem) => {
navigate(clusterViewURL({ params: { clusterId } })); navigate(clusterViewURL({ params: { clusterId } }));
}; };
render() { render() {
const { workspace } = this.props; const { workspace } = this.props;
const workspaceClusterStore = new WorkspaceClusterStore(workspace.id); const workspaceClusterStore = new WorkspaceClusterStore(workspace.id);
workspaceClusterStore.loadAll(); workspaceClusterStore.loadAll();
return ( return (
<ItemListLayout <ItemListLayout
renderHeaderTitle={<div>Clusters</div>} renderHeaderTitle={<div>Clusters</div>}
isClusterScoped isClusterScoped
isSearchable={false} isSearchable={false}
@ -42,22 +45,27 @@ export class WorkspaceOverview extends Component<Props> {
store={workspaceClusterStore} store={workspaceClusterStore}
sortingCallbacks={{ sortingCallbacks={{
[sortBy.name]: (item: ClusterItem) => item.name, [sortBy.name]: (item: ClusterItem) => item.name,
[sortBy.contextName]: (item: ClusterItem) => item.cluster.contextName, [sortBy.distribution]: (item: ClusterItem) => item.distribution,
[sortBy.version]: (item: ClusterItem) => item.cluster.version, [sortBy.version]: (item: ClusterItem) => item.version,
[sortBy.online]: (item: ClusterItem) => item.connectionStatus,
}} }}
renderTableHeader={[ renderTableHeader={[
{ title: "Name", className: "name", sortBy: sortBy.name }, { title: "Name", className: "name", sortBy: sortBy.name },
{ title: "Context", className: "context", sortBy: sortBy.contextName }, { title: "Distribution", className: "distribution", sortBy: sortBy.distribution },
{ title: "Version", className: "version", sortBy: sortBy.version }, { title: "Version", className: "version", sortBy: sortBy.version },
{ title: "Status", className: "status" }, { title: "Status", className: "status", sortBy: sortBy.online },
]} ]}
renderTableContents={(item: ClusterItem) => [ renderTableContents={(item: ClusterItem) => [
item.name, item.name,
item.cluster.contextName, item.distribution,
item.cluster.version, item.version,
item.cluster.online ? "online" : "offline" { title: item.connectionStatus, className: kebabCase(item.connectionStatus) }
]} ]}
onDetails={this.showCluster} onDetails={this.showCluster}
addRemoveButtons={{
addTooltip: "Add Cluster",
onAdd: () => navigate(addClusterURL()),
}}
renderItemMenu={(clusterItem: ClusterItem) => ( renderItemMenu={(clusterItem: ClusterItem) => (
<WorkspaceClusterMenu clusterItem={clusterItem} workspace={workspace} workspaceClusterStore={workspaceClusterStore}/> <WorkspaceClusterMenu clusterItem={clusterItem} workspace={workspace} workspaceClusterStore={workspaceClusterStore}/>
)} )}

View File

@ -24,7 +24,7 @@ export class ChooseWorkspace extends React.Component {
return { value: workspace.id, label: workspace.name }; return { value: workspace.id, label: workspace.name };
}); });
options.push({ value: ChooseWorkspace.overviewActionId, label: "Workspace: Overview" }); options.push({ value: ChooseWorkspace.overviewActionId, label: "Show current workspace overview ..." });
options.push({ value: ChooseWorkspace.addActionId, label: "Add workspace ..." }); options.push({ value: ChooseWorkspace.addActionId, label: "Add workspace ..." });

View File

@ -27,14 +27,16 @@
} }
} }
> .add-cluster { > .WorkspaceMenu {
position: relative; position: relative;
margin-bottom: $margin;
.Icon { .Icon {
margin-bottom: $margin * 1.5;
border-radius: $radius; border-radius: $radius;
padding: $padding / 3; padding: $padding / 3;
color: $addClusterIconColor; color: #ffffff66;
background: #ffffff66; background: unset;
cursor: pointer; cursor: pointer;
&.active { &.active {
@ -43,28 +45,10 @@
&:hover { &:hover {
box-shadow: none; box-shadow: none;
background: #ffffff; color: #ffffff;
background-color: unset;
} }
} }
.Badge {
$boxSize: 17px;
position: absolute;
bottom: 0px;
transform: translateX(-50%) translateY(50%);
font-size: $font-size-small;
line-height: $boxSize;
min-width: $boxSize;
min-height: $boxSize;
text-align: center;
color: white;
background: $colorSuccess;
font-weight: normal;
border-radius: $radius;
padding: 0;
pointer-events: none;
}
} }
> .extensions { > .extensions {

View File

@ -6,26 +6,24 @@ import { requestMain } from "../../../common/ipc";
import type { Cluster } from "../../../main/cluster"; import type { Cluster } from "../../../main/cluster";
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd"; import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { userStore } from "../../../common/user-store";
import { ClusterId, clusterStore } from "../../../common/cluster-store"; import { ClusterId, clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store"; import { workspaceStore } from "../../../common/workspace-store";
import { ClusterIcon } from "../cluster-icon"; import { ClusterIcon } from "../cluster-icon";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { autobind, cssNames, IClassName } from "../../utils"; import { autobind, cssNames, IClassName } from "../../utils";
import { Badge } from "../badge";
import { isActiveRoute, navigate } from "../../navigation"; import { isActiveRoute, navigate } from "../../navigation";
import { addClusterURL } from "../+add-cluster"; import { addClusterURL } from "../+add-cluster";
import { clusterSettingsURL } from "../+cluster-settings"; import { clusterSettingsURL } from "../+cluster-settings";
import { landingURL } from "../+landing-page"; import { landingURL } from "../+landing-page";
import { Tooltip } from "../tooltip";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
import { clusterViewURL } from "./cluster-view.route"; import { clusterViewURL } from "./cluster-view.route";
import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries"; import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
import { clusterDisconnectHandler } from "../../../common/cluster-ipc"; import { clusterDisconnectHandler } from "../../../common/cluster-ipc";
import { commandRegistry } from "../../../extensions/registries/command-registry"; import { commandRegistry } from "../../../extensions/registries/command-registry";
import { CommandOverlay } from "../command-palette/command-container"; import { CommandOverlay } from "../command-palette/command-container";
import { computed } from "mobx"; import { computed, observable } from "mobx";
import { Select } from "../select"; import { Select } from "../select";
import { Menu, MenuItem } from "../menu";
interface Props { interface Props {
className?: IClassName; className?: IClassName;
@ -33,14 +31,12 @@ interface Props {
@observer @observer
export class ClustersMenu extends React.Component<Props> { export class ClustersMenu extends React.Component<Props> {
@observable workspaceMenuVisible = false;
showCluster = (clusterId: ClusterId) => { showCluster = (clusterId: ClusterId) => {
navigate(clusterViewURL({ params: { clusterId } })); navigate(clusterViewURL({ params: { clusterId } }));
}; };
addCluster = () => {
navigate(addClusterURL());
};
showContextMenu = (cluster: Cluster) => { showContextMenu = (cluster: Cluster) => {
const { Menu, MenuItem } = remote; const { Menu, MenuItem } = remote;
const menu = new Menu(); const menu = new Menu();
@ -111,7 +107,6 @@ export class ClustersMenu extends React.Component<Props> {
render() { render() {
const { className } = this.props; const { className } = this.props;
const { newContexts } = userStore;
const workspace = workspaceStore.getById(workspaceStore.currentWorkspaceId); const workspace = workspaceStore.getById(workspaceStore.currentWorkspaceId);
const clusters = clusterStore.getByWorkspaceId(workspace.id).filter(cluster => cluster.enabled); const clusters = clusterStore.getByWorkspaceId(workspace.id).filter(cluster => cluster.enabled);
const activeClusterId = clusterStore.activeCluster; const activeClusterId = clusterStore.activeCluster;
@ -149,14 +144,25 @@ export class ClustersMenu extends React.Component<Props> {
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
</div> </div>
<div className="add-cluster">
<Tooltip targetId="add-cluster-icon"> <div className="WorkspaceMenu">
Add Cluster <Icon big material="menu" id="workspace-menu-icon" data-test-id="workspace-menu" />
</Tooltip> <Menu
<Icon big material="add" id="add-cluster-icon" disabled={workspace.isManaged} onClick={this.addCluster}/> usePortal
{newContexts.size > 0 && ( htmlFor="workspace-menu-icon"
<Badge className="counter" label={newContexts.size} tooltip="new"/> className="WorkspaceMenu"
)} isOpen={this.workspaceMenuVisible}
open={() => this.workspaceMenuVisible = true}
close={() => this.workspaceMenuVisible = false}
toggleEvent="click"
>
<MenuItem onClick={() => navigate(addClusterURL())} data-test-id="add-cluster-menu-item">
<Icon small material="add" /> Add Cluster
</MenuItem>
<MenuItem onClick={() => navigate(landingURL())} data-test-id="workspace-overview-menu-item">
<Icon small material="dashboard" /> Workspace Overview
</MenuItem>
</Menu>
</div> </div>
<div className="extensions"> <div className="extensions">
{globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => { {globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => {

View File

@ -17,7 +17,8 @@
left: 0; left: 0;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 24px;
height: unset;
background-color: $mainBackground; background-color: $mainBackground;
// adds extra space for traffic-light top buttons (mac only) // adds extra space for traffic-light top buttons (mac only)
@ -73,4 +74,4 @@
box-shadow: 0 0 0 1px $borderFaintColor; box-shadow: 0 0 0 1px $borderFaintColor;
} }
} }
} }