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;
const addCluster = async () => {
await utils.clickWhatsNew(app);
await utils.clickWelcomeNotification(app);
await addMinikubeCluster(app);
await waitForMinikubeDashboard(app);
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);
// Go to Pods page
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) {
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.click("div.Select__control"); // show the context drop-down list
await app.client.waitUntilTextExists("div", "minikube");

View File

@ -47,7 +47,17 @@ export async function appStart() {
export async function clickWhatsNew(app: Application) {
await app.client.waitUntilTextExists("h1", "What's new?");
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>;

View File

@ -352,7 +352,7 @@ export class AddCluster extends React.Component {
return (
<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>
{this.renderInfo()}
{this.renderKubeConfigSource()}

View File

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

View File

@ -1,65 +1,15 @@
.LandingPage {
width: 100%;
height: 100%;
.PageLayout.LandingOverview {
--width: 100%;
--height: 100%;
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 {
$bgc: $mainBackground;
$arrowSize: 10px;
.content-wrapper {
position: absolute;
left: 0;
top: 25px;
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;
}
.content {
margin: unset;
max-width: unset;
}
}
}
.PageLayout.LandingOverview {
--width: 80%;
text-align: center;
}

View File

@ -1,11 +1,13 @@
import "./landing-page.scss";
import React from "react";
import { observable } from "mobx";
import { computed, observable } from "mobx";
import { observer } from "mobx-react";
import { clusterStore } from "../../../common/cluster-store";
import { Workspace, workspaceStore } from "../../../common/workspace-store";
import { WorkspaceOverview } from "./workspace-overview";
import { PageLayout } from "../layout/page-layout";
import { Notifications } from "../notifications";
import { Icon } from "../icon";
@observer
export class LandingPage extends React.Component {
@ -15,42 +17,29 @@ export class LandingPage extends React.Component {
return workspaceStore.currentWorkspace;
}
renderStartupHint() {
return (
<div className="startup-hint flex column gaps" onMouseEnter={() => this.showHint = false}>
<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.
</p>
</div>
)
@computed
get clusters() {
return clusterStore.getByWorkspaceId(this.workspace.id);
}
componentDidMount() {
const noClustersInScope = !this.clusters.length;
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() {
const clusters = clusterStore.getByWorkspaceId(this.workspace.id);
const noClustersInScope = !clusters.length;
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>
)
}
const showBackButton = this.clusters.length > 0;
const header = <><Icon svg="logo-lens" big /> <h2>{this.workspace.name}</h2></>;
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}/>
</PageLayout>
);

View File

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

View File

@ -1,6 +1,8 @@
.WorkspaceOverview {
background-color: var(--mainBackground);
max-height: 50%;
.Table {
padding-bottom: 60px;
}
.TableCell {
display: flex;
align-items: left;
@ -10,5 +12,21 @@
flex-grow: 0.2;
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 { clusterViewURL } from "../cluster-manager/cluster-view.route";
import { WorkspaceClusterMenu } from "./workspace-cluster-menu";
import { kebabCase } from "lodash";
import { addClusterURL } from "../+add-cluster";
interface Props {
workspace: Workspace;
@ -15,8 +17,9 @@ interface Props {
enum sortBy {
name = "name",
contextName = "contextName",
distribution = "distribution",
version = "version",
online = "online"
}
@observer
@ -25,15 +28,15 @@ export class WorkspaceOverview extends Component<Props> {
showCluster = ({ clusterId }: ClusterItem) => {
navigate(clusterViewURL({ params: { clusterId } }));
};
render() {
const { workspace } = this.props;
const workspaceClusterStore = new WorkspaceClusterStore(workspace.id);
workspaceClusterStore.loadAll();
return (
<ItemListLayout
<ItemListLayout
renderHeaderTitle={<div>Clusters</div>}
isClusterScoped
isSearchable={false}
@ -42,22 +45,27 @@ export class WorkspaceOverview extends Component<Props> {
store={workspaceClusterStore}
sortingCallbacks={{
[sortBy.name]: (item: ClusterItem) => item.name,
[sortBy.contextName]: (item: ClusterItem) => item.cluster.contextName,
[sortBy.version]: (item: ClusterItem) => item.cluster.version,
[sortBy.distribution]: (item: ClusterItem) => item.distribution,
[sortBy.version]: (item: ClusterItem) => item.version,
[sortBy.online]: (item: ClusterItem) => item.connectionStatus,
}}
renderTableHeader={[
{ 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: "Status", className: "status" },
{ title: "Status", className: "status", sortBy: sortBy.online },
]}
renderTableContents={(item: ClusterItem) => [
item.name,
item.cluster.contextName,
item.cluster.version,
item.cluster.online ? "online" : "offline"
item.distribution,
item.version,
{ title: item.connectionStatus, className: kebabCase(item.connectionStatus) }
]}
onDetails={this.showCluster}
addRemoveButtons={{
addTooltip: "Add Cluster",
onAdd: () => navigate(addClusterURL()),
}}
renderItemMenu={(clusterItem: ClusterItem) => (
<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 };
});
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 ..." });

View File

@ -27,14 +27,16 @@
}
}
> .add-cluster {
> .WorkspaceMenu {
position: relative;
margin-bottom: $margin;
.Icon {
margin-bottom: $margin * 1.5;
border-radius: $radius;
padding: $padding / 3;
color: $addClusterIconColor;
background: #ffffff66;
color: #ffffff66;
background: unset;
cursor: pointer;
&.active {
@ -43,28 +45,10 @@
&:hover {
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 {

View File

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

View File

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