mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Basic workspace overview (#2047)
* 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 (#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>
This commit is contained in:
parent
2e8f94b3eb
commit
713ec8c69d
@ -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");
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -252,7 +252,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* Kubernetes version
|
* Kubernetes version
|
||||||
*/
|
*/
|
||||||
get version(): string {
|
get version(): string {
|
||||||
return String(this.metadata?.version) || "";
|
return String(this.metadata?.version || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(model: ClusterModel) {
|
constructor(model: ClusterModel) {
|
||||||
|
|||||||
@ -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()}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -1,60 +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;
|
.content-wrapper {
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: unset;
|
||||||
|
max-width: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.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;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
border-right-color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,40 +1,47 @@
|
|||||||
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 { 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
|
@observer
|
||||||
export class LandingPage extends React.Component {
|
export class LandingPage extends React.Component {
|
||||||
@observable showHint = true;
|
@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() {
|
render() {
|
||||||
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
|
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;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="LandingPage flex">
|
<PageLayout className="LandingOverview flex" header={header} provideBackButtonNavigation={showBackButton} showOnTop={true}>
|
||||||
{showStartupHint && (
|
<WorkspaceOverview workspace={this.workspace}/>
|
||||||
<div className="startup-hint flex column gaps" onMouseEnter={() => this.showHint = false}>
|
</PageLayout>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/renderer/components/+landing-page/workspace-overview.tsx
Normal file
75
src/renderer/components/+landing-page/workspace-overview.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import "./workspace-overview.scss";
|
||||||
|
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { Workspace } from "../../../common/workspace-store";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum sortBy {
|
||||||
|
name = "name",
|
||||||
|
distribution = "distribution",
|
||||||
|
version = "version",
|
||||||
|
online = "online"
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
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
|
||||||
|
renderHeaderTitle={<div>Clusters</div>}
|
||||||
|
isClusterScoped
|
||||||
|
isSearchable={false}
|
||||||
|
isSelectable={false}
|
||||||
|
className="WorkspaceOverview"
|
||||||
|
store={workspaceClusterStore}
|
||||||
|
sortingCallbacks={{
|
||||||
|
[sortBy.name]: (item: ClusterItem) => item.name,
|
||||||
|
[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: "Distribution", className: "distribution", sortBy: sortBy.distribution },
|
||||||
|
{ title: "Version", className: "version", sortBy: sortBy.version },
|
||||||
|
{ title: "Status", className: "status", sortBy: sortBy.online },
|
||||||
|
]}
|
||||||
|
renderTableContents={(item: ClusterItem) => [
|
||||||
|
item.name,
|
||||||
|
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}/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ import { clusterViewURL } from "../cluster-manager/cluster-view.route";
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ChooseWorkspace extends React.Component {
|
export class ChooseWorkspace extends React.Component {
|
||||||
|
private static overviewActionId = "__overview__";
|
||||||
private static addActionId = "__add__";
|
private static addActionId = "__add__";
|
||||||
private static removeActionId = "__remove__";
|
private static removeActionId = "__remove__";
|
||||||
private static editActionId = "__edit__";
|
private static editActionId = "__edit__";
|
||||||
@ -23,6 +24,8 @@ 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: "Show current workspace overview ..." });
|
||||||
|
|
||||||
options.push({ value: ChooseWorkspace.addActionId, label: "Add workspace ..." });
|
options.push({ value: ChooseWorkspace.addActionId, label: "Add workspace ..." });
|
||||||
|
|
||||||
if (options.length > 1) {
|
if (options.length > 1) {
|
||||||
@ -37,6 +40,13 @@ export class ChooseWorkspace extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChange(id: string) {
|
onChange(id: string) {
|
||||||
|
if (id === ChooseWorkspace.overviewActionId) {
|
||||||
|
navigate(landingURL()); // overview of active workspace. TODO: change name from landing
|
||||||
|
CommandOverlay.close();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (id === ChooseWorkspace.addActionId) {
|
if (id === ChooseWorkspace.addActionId) {
|
||||||
CommandOverlay.open(<AddWorkspace />);
|
CommandOverlay.open(<AddWorkspace />);
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 } }) => {
|
||||||
|
|||||||
@ -318,6 +318,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode {
|
renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode {
|
||||||
|
const { isSearchable, searchFilters } = this.props;
|
||||||
const { title, filters, search, info } = placeholders;
|
const { title, filters, search, info } = placeholders;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -327,7 +328,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
{this.isReady && info}
|
{this.isReady && info}
|
||||||
</div>
|
</div>
|
||||||
{filters}
|
{filters}
|
||||||
{search}
|
{isSearchable && searchFilters && search}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user