added workspaces layout, more refactoring

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman committed 2020-07-08 17:22:53 +03:00
1 parent b9089a7c5e
commit 4a06f65cb8
31 files changed
+555 -329

No files matched your search

+12 -3
View File
@@ -5,7 +5,8 @@ import { Cluster } from "../main/cluster";
import migrations from "../migrations/cluster-store"
export interface ClusterStoreModel {
clusters: ClusterModel[]
activeCluster?: ClusterId; // last opened cluster
clusters?: ClusterModel[]
}
export type ClusterId = string;
@@ -37,6 +38,7 @@ export interface ClusterPreferences {
}
export class ClusterStore extends BaseStore<ClusterStoreModel> {
@observable activeCluster: ClusterId;
@observable clusters = observable.map<ClusterId, Cluster>();
private constructor() {
@@ -69,6 +71,9 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action
removeById(clusterId: ClusterId): void {
if (this.activeCluster === clusterId) {
this.activeCluster = null;
}
this.clusters.delete(clusterId);
}
@@ -80,17 +85,21 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
@action
protected fromStore({ clusters = [] }: Partial<ClusterStoreModel> = {}) {
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
const clustersMap = new Map<ClusterId, Cluster>();
clusters.forEach(clusterModel => {
clustersMap.set(clusterModel.id, new Cluster(clusterModel));
});
this.activeCluster = clustersMap.has(activeCluster) ? activeCluster : null;
this.clusters.replace(clustersMap);
}
toJSON(): ClusterStoreModel {
const clusters = Array.from(this.clusters).map(([id, cluster]) => cluster.toJSON());
return toJS({ clusters }, {
return toJS({
activeCluster: this.activeCluster,
clusters: clusters,
}, {
recurseEverything: true
})
}
+1 -1
View File
@@ -51,7 +51,7 @@ export class Cluster implements ClusterModel {
@observable version: string;
@observable distribution: string;
@observable isAdmin: boolean;
@observable eventCount: number;
@observable eventCount: number; // todo: auto-fetch every 3s and push updates to client (?)
@observable preferences: ClusterPreferences = {};
@observable features: FeatureStatusMap = {};
+8 -9
View File
@@ -39,7 +39,6 @@ if (app.commandLine.getSwitchValue("proxy-server") !== "") {
async function main() {
shellSync(app.getLocale());
// todo: check other usages .getPath("userData") and enable "lazy-evaluation"
const workingDir = path.join(app.getPath("appData"), appName);
app.setName(appName);
app.setPath("userData", workingDir);
@@ -83,7 +82,7 @@ async function main() {
}
// manage lens windows
windowManager = new WindowManager();
windowManager = new WindowManager({showSplash: true});
windowManager.showMain(vmURL)
}
@@ -98,13 +97,13 @@ app.on('window-all-closed', function () {
if (clusterManager) clusterManager.stop()
}
})
app.on("activate", () => {
if (!windowManager) {
logger.debug("activate main window")
windowManager = new WindowManager({ showSplash: false })
windowManager.showMain(vmURL)
}
})
// app.on("activate", () => {
// if (!windowManager) {
// logger.debug("activate main window")
// windowManager = new WindowManager({ showSplash: false })
// windowManager.showMain(vmURL)
// }
// })
app.on("will-quit", async (event) => {
event.preventDefault(); // To allow mixpanel sending to be executed
if (clusterManager) clusterManager.stop()
@@ -6,6 +6,10 @@ interface Props {
export class AddCluster extends React.Component {
render() {
return "AddCluster";
return (
<div className="AddCluster">
AddCluster
</div>
)
}
}
@@ -0,0 +1 @@
export * from "./add-cluster"
@@ -0,0 +1,9 @@
.ClusterIcon {
position: relative;
.Badge {
position: absolute;
right: 0;
bottom: 0;
}
}
@@ -0,0 +1,46 @@
import "./cluster-icon.scss"
import React, { DOMAttributes } from "react";
import { observer } from "mobx-react";
import { Hashicon, HashiconProps } from "@emeraldpay/hashicon-react";
import { Cluster } from "../../../main/cluster";
import { cssNames, IClassName } from "../../utils";
import { Badge } from "../badge";
interface Props extends DOMAttributes<HTMLElement>, Omit<HashiconProps, "value"> {
className?: IClassName;
showBadge?: boolean;
cluster: Cluster;
}
const defaultProps: Partial<Props> = {
size: 38,
showBadge: true,
};
@observer
export class ClusterIcon extends React.Component<Props> {
static defaultProps = defaultProps as object;
render() {
const { className, cluster, showBadge, options, size, ...elemProps } = this.props;
const { isAdmin, eventCount, preferences } = cluster;
const { clusterName, icon } = preferences;
const eventsCount = eventCount >= 1000 ? Math.ceil(eventCount / 1000) * 1000 + "+" : eventCount;
return (
<div className={cssNames("ClusterIcon flex inline", className)} {...elemProps}>
{icon && <img src={icon} width={size} height={size} alt={clusterName}/>}
{!icon && (
<Hashicon
value={clusterName}
size={size}
options={options}
/>
)}
{showBadge && isAdmin && eventsCount && (
<Badge label={eventsCount} className="events-count"/>
)}
</div>
);
}
}
@@ -0,0 +1 @@
export * from "./cluster-settings"
@@ -1,3 +0,0 @@
.ClustersMenu {
}
@@ -1,15 +0,0 @@
import "./clusters-menu.scss"
import React from "react";
interface Props {
}
export class ClustersMenu extends React.Component {
render() {
return (
<div className="ClustersMenu">
ClustersMenu
</div>
);
}
}
@@ -1,3 +0,0 @@
.Clusters {
}
@@ -1,15 +0,0 @@
import "./clusters.scss"
import React from "react";
interface Props {
}
export class Clusters extends React.Component {
render() {
return (
<div className="Clusters">
Clusters
</div>
);
}
}
@@ -1 +0,0 @@
export * from "./clusters"
@@ -0,0 +1,15 @@
.ClustersMenu {
@include hidden-scrollbar;
padding: $padding;
background: #252729;
.add-cluster {
cursor: pointer;
opacity: 0.4;
&:hover {
opacity: .75;
}
}
}
@@ -0,0 +1,93 @@
import type { Cluster } from "../../../main/cluster";
import "./clusters-menu.scss"
import { remote } from "electron"
import React from "react";
import { observer } from "mobx-react";
import { _i18n } from "../../i18n";
import { t, Trans } from "@lingui/macro";
import { ClusterId, clusterStore } from "../../../common/cluster-store";
import { WorkspaceId } from "../../../common/workspace-store";
import { ClusterIcon } from "../+cluster-settings/cluster-icon";
import { Icon } from "../icon";
import { cssNames, IClassName } from "../../utils";
interface Props {
className?: IClassName;
workspaceId?: WorkspaceId;
}
@observer
export class ClustersMenu extends React.Component<Props> {
selectCluster = (cluster: Cluster) => {
clusterStore.activeCluster = cluster.id;
console.log('load lens for cluster:', cluster)
}
addCluster = () => {
console.log('navigate: /add-cluster')
}
showContextMenu = (clusterId: ClusterId) => {
const { Menu, MenuItem } = remote
const menu = new Menu();
menu.append(new MenuItem({
label: _i18n._(t`Settings`),
click: () => {
console.log(`navigate to cluster=${clusterId} settings`)
}
}))
// fixme: don't show item if cluster wasn't active during runtime
menu.append(new MenuItem({
label: _i18n._(t`Disconnect`),
click: () => {
console.log(`disconnect cluster=${clusterId} and navigate to landing-page`)
}
}))
menu.popup({
window: remote.getCurrentWindow()
})
}
// fixme: allow to rearrange clusters with drag&drop
render() {
const { workspaceId, className } = this.props;
const clusters = clusterStore.getByWorkspaceId(workspaceId);
const addClusterTooltip = (
<div className="flex column gaps">
<p><Trans>This is the quick launch menu.</Trans></p>
<p>
<Trans>
Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button.
</Trans>
</p>
</div>
)
return (
<div className={cssNames("ClustersMenu flex gaps column", className)}>
{clusters.map(cluster => {
const isActive = cluster.id === clusterStore.activeCluster;
return (
<ClusterIcon
key={cluster.id}
cluster={cluster}
className={cssNames({ active: isActive })}
onClick={() => this.selectCluster(cluster)}
onContextMenu={() => this.showContextMenu(cluster.id)}
/>
)
})}
{/* todo: add badge for "newContexts" since last visit */}
{/* fixme: make tooltip visible on init + remove following to mouse pos */}
<Icon
big material="add"
className="add-cluster"
tooltip={{ children: addClusterTooltip }}
onClick={this.addCluster}
/>
</div>
);
}
}
@@ -1,3 +1,41 @@
.Workspaces {
height: 100%;
}
display: grid;
grid-template-areas: "draggable draggable" "menu lens-view" "bottom-bar bottom-bar";
grid-template-rows: 20px 1fr min-content;
grid-template-columns: min-content 1fr;
> .draggable-top {
@include set-draggable;
grid-area: draggable;
}
> .ClusterMenu {
grid-area: menu;
}
> .lens-container {
grid-area: lens-view;
}
> .bottom-bar {
grid-area: bottom-bar;
font-size: $font-size-small;
background-color: #3d90ce;
padding: $padding / 2 $padding;
color: white;
#workspace {
cursor: pointer;
}
}
}
.WorkspacesMenu {
border-radius: $radius;
.workspaces-title {
padding: $padding;
}
}
@@ -1,14 +1,61 @@
import "./workspaces.scss"
import React from "react";
import { observer } from "mobx-react";
import { Link } from "react-router-dom";
import { Trans } from "@lingui/macro";
import { workspaceStore } from "../../../common/workspace-store";
import { Icon } from "../icon";
import { ClustersMenu } from "./clusters-menu";
import { Menu, MenuItem } from "../menu";
import { prevDefault } from "../../utils";
import { observable } from "mobx";
interface Props {
}
// todo: support `workspaceId` in URL
@observer
export class Workspaces extends React.Component {
@observable menuVisible = false;
render() {
const { currentWorkspace, workspaces } = workspaceStore;
return (
<div className="Workspaces">
Workspaces
<div className="draggable-top"/>
<ClustersMenu/>
<div className="lens-container">
{/*todo: replace with BrowserView */}
</div>
<div className="bottom-bar flex justify-flex-end">
<div id="workspace" className="workspace flex align-center">
<Icon small material="layers"/> {currentWorkspace}
</div>
<Menu
usePortal
htmlFor="workspace"
className="WorkspacesMenu"
isOpen={this.menuVisible}
open={() => this.menuVisible = true}
close={() => this.menuVisible = false}
>
<Link
to="/workspaces"
className="workspaces-title"
onClick={prevDefault(() => console.log('/navigate: workspaces page'))}>
<Trans>Workspaces</Trans>
</Link>
{Array.from(workspaces.values()).map(workspace => {
const { id, name, description } = workspace;
return (
<MenuItem key={id} onClick={() => console.log(`navigate: /workspaces/${id}`)} title={description}>
<Icon small material="layers"/> {name}
</MenuItem>
)
})}
</Menu>
</div>
</div>
)
}
+5
View File
@@ -12,6 +12,11 @@
-webkit-font-smoothing: antialiased;
}
:root {
--mainBackground: #1e2124;
--textColorPrimary: #87909c;
}
::selection {
background: $primary;
color: white;
+1 -1
View File
@@ -1,2 +1,2 @@
export * from './menu'
export * from './menu-picker'
export * from './menu-actions'
@@ -1,74 +0,0 @@
.MenuPicker {
&.waiting {
.MenuItem {
pointer-events: none;
opacity: .75;
}
}
> .Button {
padding: $padding /2 $padding * 2;
}
.menu-header {
background: var(--bgc-active);
padding: $padding;
display: flex;
align-items: center;
color: $textColorAccent;
font-weight: bold;
&:hover {
background: var(--bgc-active) !important;
}
}
.Menu {
@include custom-scrollbar;
border-radius: $radius;
box-shadow: -$unit / 2 $unit / 2 22px $boxShadow;
margin-top: ceil($margin / 1.33);
overflow: hidden;
min-width: 150px;
max-height: $unit * 35;
}
.MenuItem {
align-items: center;
display: flex;
padding-right: $padding;
white-space: normal;
&:before {
min-width: 20px;
font-family: "Material Icons";
font-weight: bold;
content: ' '
}
&:hover {
background: $colorInfo;
}
&.active {
font-weight: bold;
&:not(:hover) {
background: inherit;
}
&:before {
content: 'check';
}
}
&:focus {
outline: none
}
&:not(:last-child) {
border-bottom: 1px solid $borderColor;
}
}
}
@@ -1,42 +0,0 @@
import './menu-picker.scss'
import React, { useRef, useState } from "react";
import { cssNames } from "../../utils";
import { Menu, MenuProps } from "./menu";
import { Icon } from "../icon";
import { Button } from "../button";
import uniqueId from "lodash/uniqueId";
interface Props extends Partial<MenuProps> {
title: React.ReactNode;
waiting?: boolean;
}
export function MenuPicker(props: Props) {
const id = useRef(uniqueId("menu_picker_")).current;
const { className, title, waiting, children, ...menuProps } = props;
const [isOpen, setOpen] = useState(false);
const toggle = () => setOpen(!isOpen);
return (
<div className={cssNames("MenuPicker", className, { waiting })}>
<Button primary id={id}>
{title}
<Icon material="arrow_drop_down"/>
</Button>
<Menu
htmlFor={id}
isOpen={isOpen} open={toggle} close={toggle}
closeOnClickItem={false}
{...menuProps}
>
<div className="menu-header flex gaps">
<span className="box grow">{title}</span>
<Icon small material="close" onClick={toggle}/>
</div>
{children}
</Menu>
</div>
)
}
+3 -2
View File
@@ -90,8 +90,9 @@
cursor: pointer;
}
@mixin set-draggable($isDraggable: true) {
@if ($isDraggable) {
@mixin set-draggable($is-draggable: true) {
@if ($is-draggable) {
cursor: move;
-webkit-user-drag: auto;
-webkit-app-region: drag;
} @else {
@@ -7,6 +7,9 @@ import { createPortal } from "react-dom"
import { autobind, cssNames } from "../../utils";
import { Animate } from "../animate";
// fixme: better positioning + remove "flying effect"
// todo: add flag to show visible tooltip by default (until mouse-over the target)
export interface TooltipProps {
htmlFor: string;
className?: string;
-5
View File
@@ -1,11 +1,6 @@
@import "colors", "media";
@import "../themes/theme-vars";
:root {
--mainBackground: #1e2124;
--textColorPrimary: #87909c;
}
// Dimensions
$unit: 8px;
$padding: $unit;
+29 -9
View File
@@ -1,18 +1,38 @@
// todo: remove when app.tsx re-used
import "./components/app.scss"
import "./theme.store";
import "../common/system-ca"
import React from "react";
import ReactDOM from "react-dom";
import "../common/system-ca"
import { Router } from "react-router";
import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store";
import { clusterStore } from "../common/cluster-store";
import { Workspaces } from "./components/+workspaces/workspaces";
import { I18nProvider } from "@lingui/react";
import { _i18n } from "./i18n";
import { browserHistory } from "./navigation";
async function render() {
await Promise.all([
userStore.load(),
workspaceStore.load(),
clusterStore.load(),
]);
ReactDOM.render(<Workspaces/>, document.getElementById("app"),)
class App extends React.Component {
static async init() {
await Promise.all([
userStore.load(),
workspaceStore.load(),
clusterStore.load(),
]);
ReactDOM.render(<App/>, document.getElementById("app"),)
}
render() {
return (
<I18nProvider i18n={_i18n}>
<Router history={browserHistory}>
<Workspaces/>
</Router>
</I18nProvider>
)
}
}
window.addEventListener("load", render);
window.addEventListener("load", App.init);