diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts index e73774f86a..1662f33f4a 100644 --- a/integration/__tests__/cluster-pages.tests.ts +++ b/integration/__tests__/cluster-pages.tests.ts @@ -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"); diff --git a/integration/helpers/minikube.ts b/integration/helpers/minikube.ts index 67ef0145d5..e2ca0e0f23 100644 --- a/integration/helpers/minikube.ts +++ b/integration/helpers/minikube.ts @@ -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"); diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index f96c9124e2..1db9af1c4d 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -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; diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 38d03482e8..dc5cdf4d4c 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -352,7 +352,7 @@ export class AddCluster extends React.Component { return ( - Add Clusters}> +

Add Clusters

} showOnTop={true}>

Add Clusters from Kubeconfig

{this.renderInfo()} {this.renderKubeConfigSource()} diff --git a/src/renderer/components/+cluster-settings/cluster-settings.tsx b/src/renderer/components/+cluster-settings/cluster-settings.tsx index 0cde390c47..4ec8e1ccdd 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx @@ -59,7 +59,7 @@ export class ClusterSettings extends React.Component { ); return ( - + diff --git a/src/renderer/components/+landing-page/landing-page.scss b/src/renderer/components/+landing-page/landing-page.scss index 3610186e26..ea1eb664ef 100644 --- a/src/renderer/components/+landing-page/landing-page.scss +++ b/src/renderer/components/+landing-page/landing-page.scss @@ -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; -} \ No newline at end of file diff --git a/src/renderer/components/+landing-page/landing-page.tsx b/src/renderer/components/+landing-page/landing-page.tsx index 3bb50e97e5..15e22b2c9f 100644 --- a/src/renderer/components/+landing-page/landing-page.tsx +++ b/src/renderer/components/+landing-page/landing-page.tsx @@ -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 ( -
this.showHint = false}> -

This is the quick launch menu.

-

- Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button. -

-
- ) + @computed + get clusters() { + return clusterStore.getByWorkspaceId(this.workspace.id); + } + + componentDidMount() { + const noClustersInScope = !this.clusters.length; + const showStartupHint = this.showHint; + + if (showStartupHint && noClustersInScope) { + Notifications.info(<>Welcome!

Get started by associating one or more clusters to Lens

, { + 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 =

Workspace: {this.workspace.name}

; - - if (this.workspace.name === "default" && noClustersInScope) { - return ( -
- {showStartupHint && this.renderStartupHint()} -
-

- Welcome! -

-

- Get started by associating one or more clusters to Lens. -

-
-
- ) - } + const showBackButton = this.clusters.length > 0; + const header = <>

{this.workspace.name}

; return ( - + ); diff --git a/src/renderer/components/+landing-page/workspace-cluster.store.ts b/src/renderer/components/+landing-page/workspace-cluster.store.ts index 35b4f64880..1117528a99 100644 --- a/src/renderer/components/+landing-page/workspace-cluster.store.ts +++ b/src/renderer/components/+landing-page/workspace-cluster.store.ts @@ -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 { () => ( clusterStore .getByWorkspaceId(this.workspaceId) + .filter(cluster => cluster.enabled) .map(cluster => new ClusterItem(cluster)) ) ); @@ -56,8 +69,4 @@ export class WorkspaceClusterStore extends ItemStore { return super.removeItem(clusterItem, () => clusterStore.removeById(clusterId)); } } - - async removeSelectedItems() { - return Promise.all(this.selectedItems.map(this.remove)); - } } diff --git a/src/renderer/components/+landing-page/workspace-overview.scss b/src/renderer/components/+landing-page/workspace-overview.scss index 8b341e62a7..fa156bf74f 100644 --- a/src/renderer/components/+landing-page/workspace-overview.scss +++ b/src/renderer/components/+landing-page/workspace-overview.scss @@ -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); + } } -} \ No newline at end of file + + .TableCell.status { + flex: 0.1; + } + + .TableCell.distribution { + flex: 0.2; + } + + .TableCell.version { + flex: 0.2; + } +} diff --git a/src/renderer/components/+landing-page/workspace-overview.tsx b/src/renderer/components/+landing-page/workspace-overview.tsx index b245c2294b..c095abb31b 100644 --- a/src/renderer/components/+landing-page/workspace-overview.tsx +++ b/src/renderer/components/+landing-page/workspace-overview.tsx @@ -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 { showCluster = ({ clusterId }: ClusterItem) => { navigate(clusterViewURL({ params: { clusterId } })); }; - + render() { const { workspace } = this.props; const workspaceClusterStore = new WorkspaceClusterStore(workspace.id); - + workspaceClusterStore.loadAll(); return ( - Clusters} isClusterScoped isSearchable={false} @@ -42,22 +45,27 @@ export class WorkspaceOverview extends Component { 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) => ( )} diff --git a/src/renderer/components/+workspaces/workspaces.tsx b/src/renderer/components/+workspaces/workspaces.tsx index 9c7167a826..3bea020ef3 100644 --- a/src/renderer/components/+workspaces/workspaces.tsx +++ b/src/renderer/components/+workspaces/workspaces.tsx @@ -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 ..." }); diff --git a/src/renderer/components/cluster-manager/clusters-menu.scss b/src/renderer/components/cluster-manager/clusters-menu.scss index b6a1e66d1a..b24daa4522 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.scss +++ b/src/renderer/components/cluster-manager/clusters-menu.scss @@ -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 { diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index d963438136..dd36e529e1 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -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 { + @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 { 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 { -
- - Add Cluster - - - {newContexts.size > 0 && ( - - )} + +
+ + this.workspaceMenuVisible = true} + close={() => this.workspaceMenuVisible = false} + toggleEvent="click" + > + navigate(addClusterURL())} data-test-id="add-cluster-menu-item"> + Add Cluster + + navigate(landingURL())} data-test-id="workspace-overview-menu-item"> + Workspace Overview + +
{globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => { diff --git a/src/renderer/components/layout/page-layout.scss b/src/renderer/components/layout/page-layout.scss index c975ea3305..d7fd0a8544 100644 --- a/src/renderer/components/layout/page-layout.scss +++ b/src/renderer/components/layout/page-layout.scss @@ -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; } } -} \ No newline at end of file +}