Skip to content

Commit

Permalink
Basic workspace overview (lensapp#2047)
Browse files Browse the repository at this point in the history
* basic workspace overview

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

* css tweaks for landing page as a PageLayout

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

* address review comments

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

* more review comment addressing, added overview to workspace command palette

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

* added back the landing page startup hint

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

* refactoring as per review comments

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

* added original landing page back only for default workspace with no clusters

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

* Workspace overview layout tweaks (lensapp#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>

* address review comments, and rebased to master

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

Co-authored-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
Co-authored-by: Jim Ehrismann <jehrismann@miranits.com>
  • Loading branch information
3 people committed Mar 11, 2021
1 parent 2e8f94b commit 713ec8c
Show file tree
Hide file tree
Showing 17 changed files with 355 additions and 127 deletions.
3 changes: 2 additions & 1 deletion integration/__tests__/cluster-pages.tests.ts
Expand Up @@ -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"]');
Expand Down Expand Up @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion integration/helpers/minikube.ts
Expand Up @@ -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");
Expand Down
12 changes: 11 additions & 1 deletion integration/helpers/utils.ts
Expand Up @@ -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>;
Expand Down
2 changes: 1 addition & 1 deletion src/main/cluster.ts
Expand Up @@ -252,7 +252,7 @@ export class Cluster implements ClusterModel, ClusterState {
* Kubernetes version
*/
get version(): string {
return String(this.metadata?.version) || "";
return String(this.metadata?.version || "");
}

constructor(model: ClusterModel) {
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/+add-cluster/add-cluster.tsx
Expand Up @@ -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()}
Expand Down
Expand Up @@ -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>
Expand Down
61 changes: 8 additions & 53 deletions src/renderer/components/+landing-page/landing-page.scss
@@ -1,60 +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;

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;
.content-wrapper {

&:before {
border-right-color: white;
}
.content {
margin: unset;
max-width: unset;
}
}
}
}
57 changes: 32 additions & 25 deletions src/renderer/components/+landing-page/landing-page.tsx
@@ -1,40 +1,47 @@
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 { workspaceStore } from "../../../common/workspace-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 {
@observable showHint = true;

get workspace(): Workspace {
return workspaceStore.currentWorkspace;
}

@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(workspaceStore.currentWorkspaceId);
const noClustersInScope = !clusters.length;
const showStartupHint = this.showHint && noClustersInScope;
const showBackButton = this.clusters.length > 0;
const header = <><Icon svg="logo-lens" big /> <h2>{this.workspace.name}</h2></>;

return (
<div className="LandingPage flex">
{showStartupHint && (
<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>
)}
{noClustersInScope && (
<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>
<PageLayout className="LandingOverview flex" header={header} provideBackButtonNavigation={showBackButton} showOnTop={true}>
<WorkspaceOverview workspace={this.workspace}/>
</PageLayout>
);
}
}
74 changes: 74 additions & 0 deletions src/renderer/components/+landing-page/workspace-cluster-menu.tsx
@@ -0,0 +1,74 @@
import React from "react";
import { ClusterItem, WorkspaceClusterStore } from "./workspace-cluster.store";
import { autobind, cssNames } from "../../utils";
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
import { MenuItem } from "../menu";
import { Icon } from "../icon";
import { Workspace } from "../../../common/workspace-store";
import { clusterSettingsURL } from "../+cluster-settings";
import { navigate } from "../../navigation";

interface Props extends MenuActionsProps {
clusterItem: ClusterItem;
workspace: Workspace;
workspaceClusterStore: WorkspaceClusterStore;
}

export class WorkspaceClusterMenu extends React.Component<Props> {

@autobind()
remove() {
const { clusterItem, workspaceClusterStore } = this.props;

return workspaceClusterStore.remove(clusterItem);
}

@autobind()
gotoSettings() {
const { clusterItem } = this.props;

navigate(clusterSettingsURL({
params: {
clusterId: clusterItem.id
}
}));
}

@autobind()
renderRemoveMessage() {
const { clusterItem, workspace } = this.props;

return (
<p>Remove cluster <b>{clusterItem.name}</b> from workspace <b>{workspace.name}</b>?</p>
);
}


renderContent() {
const { toolbar } = this.props;

return (
<>
<MenuItem onClick={this.gotoSettings}>
<Icon material="settings" interactive={toolbar} title="Settings"/>
<span className="title">Settings</span>
</MenuItem>
</>
);
}

render() {
const { clusterItem: { cluster: { isManaged } }, className, ...menuProps } = this.props;

return (
<MenuActions
{...menuProps}
className={cssNames("WorkspaceClusterMenu", className)}
removeAction={isManaged ? null : this.remove}
removeConfirmationMessage={this.renderRemoveMessage}
>
{this.renderContent()}
</MenuActions>
);
}
}
72 changes: 72 additions & 0 deletions src/renderer/components/+landing-page/workspace-cluster.store.ts
@@ -0,0 +1,72 @@
import { WorkspaceId } from "../../../common/workspace-store";
import { Cluster } from "../../../main/cluster";
import { clusterStore } from "../../../common/cluster-store";
import { ItemObject, ItemStore } from "../../item.store";
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?.toString() ?? "unknown";
}

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;
}
}

/** an ItemStore of the clusters belonging to a given workspace */
@autobind()
export class WorkspaceClusterStore extends ItemStore<ClusterItem> {

workspaceId: WorkspaceId;

constructor(workspaceId: WorkspaceId) {
super();
this.workspaceId = workspaceId;
}

loadAll() {
return this.loadItems(
() => (
clusterStore
.getByWorkspaceId(this.workspaceId)
.filter(cluster => cluster.enabled)
.map(cluster => new ClusterItem(cluster))
)
);
}

async remove(clusterItem: ClusterItem) {
const { cluster: { isManaged, id: clusterId }} = clusterItem;

if (!isManaged) {
return super.removeItem(clusterItem, () => clusterStore.removeById(clusterId));
}
}
}
32 changes: 32 additions & 0 deletions src/renderer/components/+landing-page/workspace-overview.scss
@@ -0,0 +1,32 @@
.WorkspaceOverview {
max-height: 50%;
.Table {
padding-bottom: 60px;
}
.TableCell {
display: flex;
align-items: left;

&.cluster-icon {
align-items: center;
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;
}
}

0 comments on commit 713ec8c

Please sign in to comment.