mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Migrating Vue components to React and stores refactoring (#585)
Signed-off-by: Roman <ixrock@gmail.com> Co-authored-by: Sebastian Malton <sebastian@malton.name> Co-authored-by: Sebastian Malton <smalton@mirantis.com> Co-authored-by: Lauri Nevala <lauri.nevala@gmail.com> Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
297 files changed
+8186
-9103
No files matched your search
@@ -0,0 +1,13 @@
|
||||
.BottomBar {
|
||||
$spacing: $padding / 2;
|
||||
|
||||
font-size: $font-size-small;
|
||||
background-color: #3d90ce;
|
||||
padding: $spacing $padding;
|
||||
color: white;
|
||||
|
||||
#current-workspace {
|
||||
--flex-gap: #{$spacing};
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import "./bottom-bar.scss"
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Icon } from "../icon";
|
||||
import { WorkspaceMenu } from "../+workspaces/workspace-menu";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
|
||||
@observer
|
||||
export class BottomBar extends React.Component {
|
||||
render() {
|
||||
const { currentWorkspace } = workspaceStore;
|
||||
return (
|
||||
<div className="BottomBar flex gaps">
|
||||
<div id="current-workspace" className="flex gaps align-center box right">
|
||||
<Icon small material="layers"/>
|
||||
<span className="workspace-name">{currentWorkspace.name}</span>
|
||||
</div>
|
||||
<WorkspaceMenu htmlFor="current-workspace"/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
.ClusterManager {
|
||||
display: grid;
|
||||
grid-template-areas: "menu main" "menu main" "bottom-bar bottom-bar";
|
||||
grid-template-rows: auto 1fr min-content;
|
||||
grid-template-columns: min-content 1fr;
|
||||
height: 100%;
|
||||
|
||||
main {
|
||||
grid-area: main;
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
> * {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
background-color: $mainBackground;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ClustersMenu {
|
||||
grid-area: menu;
|
||||
}
|
||||
|
||||
.BottomBar {
|
||||
grid-area: bottom-bar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import "./cluster-manager.scss"
|
||||
import React from "react";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { ClustersMenu } from "./clusters-menu";
|
||||
import { BottomBar } from "./bottom-bar";
|
||||
import { LandingPage, landingRoute, landingURL } from "../+landing-page";
|
||||
import { Preferences, preferencesRoute } from "../+preferences";
|
||||
import { Workspaces, workspacesRoute } from "../+workspaces";
|
||||
import { AddCluster, addClusterRoute } from "../+add-cluster";
|
||||
import { ClusterView } from "./cluster-view";
|
||||
import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
|
||||
import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
||||
|
||||
@observer
|
||||
export class ClusterManager extends React.Component {
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(getMatchedClusterId, initView, {
|
||||
fireImmediately: true
|
||||
}),
|
||||
reaction(() => [
|
||||
hasLoadedView(getMatchedClusterId()), // refresh when cluster's webview loaded
|
||||
getMatchedCluster()?.available, // refresh on disconnect active-cluster
|
||||
], refreshViews, {
|
||||
fireImmediately: true
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
lensViews.clear();
|
||||
}
|
||||
|
||||
get startUrl() {
|
||||
const { activeClusterId } = clusterStore;
|
||||
if (activeClusterId) {
|
||||
return clusterViewURL({
|
||||
params: {
|
||||
clusterId: activeClusterId
|
||||
}
|
||||
})
|
||||
}
|
||||
return landingURL()
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ClusterManager">
|
||||
<div id="draggable-top"/>
|
||||
<main>
|
||||
<div id="lens-views"/>
|
||||
<Switch>
|
||||
<Route component={LandingPage} {...landingRoute}/>
|
||||
<Route component={Preferences} {...preferencesRoute}/>
|
||||
<Route component={Workspaces} {...workspacesRoute}/>
|
||||
<Route component={AddCluster} {...addClusterRoute}/>
|
||||
<Route component={ClusterView} {...clusterViewRoute}/>
|
||||
<Route component={ClusterSettings} {...clusterSettingsRoute}/>
|
||||
<Redirect exact to={this.startUrl}/>
|
||||
</Switch>
|
||||
</main>
|
||||
<ClustersMenu/>
|
||||
<BottomBar/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.ClusterStatus {
|
||||
--flex-gap: #{$padding * 2};
|
||||
|
||||
position: relative;
|
||||
min-width: 350px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
|
||||
pre {
|
||||
@include hidden-scrollbar;
|
||||
max-width: 70vw;
|
||||
max-height: 40vh;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.Icon {
|
||||
--size: 70px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { KubeAuthProxyLog } from "../../../main/kube-auth-proxy";
|
||||
|
||||
import "./cluster-status.scss"
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { computed, observable } from "mobx";
|
||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
||||
import { Icon } from "../icon";
|
||||
import { Button } from "../button";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { ClusterId, clusterStore } from "../../../common/cluster-store";
|
||||
import { CubeSpinner } from "../spinner";
|
||||
|
||||
interface Props {
|
||||
className?: IClassName;
|
||||
clusterId: ClusterId;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterStatus extends React.Component<Props> {
|
||||
@observable authOutput: KubeAuthProxyLog[] = [];
|
||||
@observable isReconnecting = false;
|
||||
|
||||
get cluster(): Cluster {
|
||||
return clusterStore.getById(this.props.clusterId);
|
||||
}
|
||||
|
||||
@computed get hasErrors(): boolean {
|
||||
return this.authOutput.some(({ error }) => error) || !!this.cluster.failureReason;
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyLog) => {
|
||||
this.authOutput.push({
|
||||
data: res.data.trimRight(),
|
||||
error: res.error,
|
||||
});
|
||||
})
|
||||
if (!this.cluster.initialized || this.cluster.disconnected) {
|
||||
await this.refreshCluster();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ipcRenderer.removeAllListeners(`kube-auth:${this.props.clusterId}`);
|
||||
}
|
||||
|
||||
refreshCluster = async () => {
|
||||
await clusterIpc.activate.invokeFromRenderer(this.props.clusterId);
|
||||
}
|
||||
|
||||
reconnect = async () => {
|
||||
this.isReconnecting = true;
|
||||
await this.refreshCluster();
|
||||
this.isReconnecting = false;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const { authOutput, cluster, hasErrors } = this;
|
||||
const failureReason = cluster.failureReason;
|
||||
if (!hasErrors || this.isReconnecting) {
|
||||
return (
|
||||
<>
|
||||
<CubeSpinner />
|
||||
<pre className="kube-auth-out">
|
||||
<p>{this.isReconnecting ? "Reconnecting..." : "Connecting..."}</p>
|
||||
{authOutput.map(({ data, error }, index) => {
|
||||
return <p key={index} className={cssNames({ error })}>{data}</p>
|
||||
})}
|
||||
</pre>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Icon material="cloud_off" className="error" />
|
||||
<h2>
|
||||
{cluster.preferences.clusterName}
|
||||
</h2>
|
||||
<pre className="kube-auth-out">
|
||||
{authOutput.map(({ data, error }, index) => {
|
||||
return <p key={index} className={cssNames({ error })}>{data}</p>
|
||||
})}
|
||||
</pre>
|
||||
{failureReason && (
|
||||
<div className="failure-reason error">{failureReason}</div>
|
||||
)}
|
||||
<Button
|
||||
primary
|
||||
label="Reconnect"
|
||||
className="box center"
|
||||
onClick={this.reconnect}
|
||||
waiting={this.isReconnecting}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={cssNames("ClusterStatus flex column gaps box center align-center justify-center", this.props.className)}>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { reaction } from "mobx";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { matchPath, RouteProps } from "react-router";
|
||||
import { buildURL, navigation } from "../../navigation";
|
||||
import { clusterStore, getHostedClusterId } from "../../../common/cluster-store";
|
||||
import { clusterSettingsRoute } from "../+cluster-settings/cluster-settings.route";
|
||||
|
||||
export interface IClusterViewRouteParams {
|
||||
clusterId: string;
|
||||
}
|
||||
|
||||
export const clusterViewRoute: RouteProps = {
|
||||
exact: true,
|
||||
path: "/cluster/:clusterId",
|
||||
}
|
||||
|
||||
export const clusterViewURL = buildURL<IClusterViewRouteParams>(clusterViewRoute.path)
|
||||
|
||||
export function getMatchedClusterId(): string {
|
||||
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
|
||||
exact: true,
|
||||
path: [
|
||||
clusterViewRoute.path,
|
||||
clusterSettingsRoute.path,
|
||||
].flat(),
|
||||
})
|
||||
if (matched) {
|
||||
return matched.params.clusterId;
|
||||
}
|
||||
}
|
||||
|
||||
export function getMatchedCluster() {
|
||||
return clusterStore.getById(getMatchedClusterId())
|
||||
}
|
||||
|
||||
// Refresh global menu depending on active route's type (common/cluster view)
|
||||
if (ipcRenderer) {
|
||||
const isMainView = !getHostedClusterId();
|
||||
if (isMainView) {
|
||||
reaction(() => getMatchedClusterId(), clusterId => {
|
||||
ipcRenderer.send("cluster-view:change", clusterId);
|
||||
}, {
|
||||
fireImmediately: true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.ClusterView {
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import "./cluster-view.scss"
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { getMatchedCluster } from "./cluster-view.route";
|
||||
import { ClusterStatus } from "./cluster-status";
|
||||
import { hasLoadedView } from "./lens-views";
|
||||
|
||||
@observer
|
||||
export class ClusterView extends React.Component {
|
||||
render() {
|
||||
const cluster = getMatchedCluster();
|
||||
const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id))
|
||||
return (
|
||||
<div className="ClusterView">
|
||||
{showStatus && (
|
||||
<ClusterStatus key={cluster.id} clusterId={cluster.id} className="box center"/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
.ClustersMenu {
|
||||
@include hidden-scrollbar;
|
||||
$spacing: $padding * 2;
|
||||
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding: $spacing;
|
||||
background: var(--clusters-menu-bgc);
|
||||
|
||||
.is-mac & {
|
||||
padding-top: $spacing * 2;
|
||||
}
|
||||
|
||||
> .startup-tooltip {
|
||||
$bgc: $mainBackground;
|
||||
$arrowSize: 10px;
|
||||
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 100%;
|
||||
margin: $padding;
|
||||
padding: $spacing;
|
||||
width: 320px;
|
||||
background: $bgc;
|
||||
z-index: 100;
|
||||
color: white;
|
||||
filter: drop-shadow(0 0px 2px #ffffff33);
|
||||
pointer-events: none;
|
||||
|
||||
&: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%;
|
||||
}
|
||||
}
|
||||
|
||||
> .add-cluster {
|
||||
position: relative;
|
||||
margin-top: $padding;
|
||||
min-width: 43px;
|
||||
|
||||
.Icon {
|
||||
border-radius: $radius;
|
||||
padding: $padding / 3;
|
||||
color: var(--clusters-menu-bgc) !important;
|
||||
background: white !important;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
opacity: 0.4;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
opacity: .75;
|
||||
}
|
||||
}
|
||||
|
||||
.Badge {
|
||||
$boxSize: 17px;
|
||||
$offset: -7px;
|
||||
|
||||
position: absolute;
|
||||
font-size: $font-size-small;
|
||||
right: $offset;
|
||||
bottom: $offset;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import "./clusters-menu.scss"
|
||||
import { remote } from "electron"
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { observable } from "mobx";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
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 { cssNames, IClassName } from "../../utils";
|
||||
import { Badge } from "../badge";
|
||||
import { navigate, navigation } 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 { clusterIpc } from "../../../common/cluster-ipc";
|
||||
import { clusterViewURL, getMatchedClusterId } from "./cluster-view.route";
|
||||
|
||||
// fixme: allow to rearrange clusters with drag&drop
|
||||
|
||||
interface Props {
|
||||
className?: IClassName;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClustersMenu extends React.Component<Props> {
|
||||
@observable showHint = true;
|
||||
|
||||
showCluster = (clusterId: ClusterId) => {
|
||||
clusterStore.setActive(clusterId);
|
||||
navigate(clusterViewURL({ params: { clusterId } }));
|
||||
}
|
||||
|
||||
addCluster = () => {
|
||||
navigate(addClusterURL());
|
||||
}
|
||||
|
||||
showContextMenu = (cluster: Cluster) => {
|
||||
const { Menu, MenuItem } = remote
|
||||
const menu = new Menu();
|
||||
|
||||
menu.append(new MenuItem({
|
||||
label: _i18n._(t`Settings`),
|
||||
click: () => {
|
||||
navigate(clusterSettingsURL({
|
||||
params: {
|
||||
clusterId: cluster.id
|
||||
}
|
||||
}))
|
||||
}
|
||||
}));
|
||||
if (cluster.online) {
|
||||
menu.append(new MenuItem({
|
||||
label: _i18n._(t`Disconnect`),
|
||||
click: async () => {
|
||||
if (clusterStore.isActive(cluster.id)) {
|
||||
navigate(landingURL());
|
||||
}
|
||||
await clusterIpc.disconnect.invokeFromRenderer(cluster.id);
|
||||
}
|
||||
}))
|
||||
}
|
||||
menu.append(new MenuItem({
|
||||
label: _i18n._(t`Remove`),
|
||||
click: () => {
|
||||
ConfirmDialog.open({
|
||||
okButtonProps: {
|
||||
primary: false,
|
||||
accent: true,
|
||||
label: _i18n._(t`Remove`),
|
||||
},
|
||||
ok: () => {
|
||||
clusterStore.removeById(cluster.id);
|
||||
navigate(landingURL());
|
||||
},
|
||||
message: <p>Are you sure want to remove cluster <b title={cluster.id}>{cluster.contextName}</b>?</p>,
|
||||
})
|
||||
}
|
||||
}));
|
||||
menu.popup({
|
||||
window: remote.getCurrentWindow()
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
const { newContexts } = userStore;
|
||||
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
|
||||
const noClustersInScope = clusters.length === 0;
|
||||
const isLanding = navigation.getPath() === landingURL();
|
||||
const showStartupHint = this.showHint && isLanding && noClustersInScope;
|
||||
return (
|
||||
<div
|
||||
className={cssNames("ClustersMenu flex column gaps", className)}
|
||||
onMouseEnter={() => this.showHint = false}
|
||||
>
|
||||
{showStartupHint && (
|
||||
<div className="startup-tooltip 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>
|
||||
)}
|
||||
{clusters.map(cluster => {
|
||||
return (
|
||||
<ClusterIcon
|
||||
key={cluster.id}
|
||||
showErrors={true}
|
||||
cluster={cluster}
|
||||
isActive={cluster.id === getMatchedClusterId()}
|
||||
onClick={() => this.showCluster(cluster.id)}
|
||||
onContextMenu={() => this.showContextMenu(cluster)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<div className="add-cluster" onClick={this.addCluster}>
|
||||
<Tooltip targetId="add-cluster-icon">
|
||||
<Trans>Add Cluster</Trans>
|
||||
</Tooltip>
|
||||
<Icon big material="add" id="add-cluster-icon"/>
|
||||
{newContexts.size > 0 && (
|
||||
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>}/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./cluster-manager"
|
||||
@@ -0,0 +1,43 @@
|
||||
import { observable } from "mobx";
|
||||
import { ClusterId, clusterStore } from "../../../common/cluster-store";
|
||||
import { getMatchedCluster } from "./cluster-view.route"
|
||||
import logger from "../../../main/logger";
|
||||
|
||||
export interface LensView {
|
||||
isLoaded?: boolean
|
||||
clusterId: ClusterId;
|
||||
view: HTMLIFrameElement
|
||||
}
|
||||
|
||||
export const lensViews = observable.map<ClusterId, LensView>();
|
||||
|
||||
export function hasLoadedView(clusterId: ClusterId): boolean {
|
||||
return !!lensViews.get(clusterId)?.isLoaded;
|
||||
}
|
||||
|
||||
export async function initView(clusterId: ClusterId) {
|
||||
if (!clusterId || lensViews.has(clusterId)) {
|
||||
return;
|
||||
}
|
||||
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`)
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
await cluster.whenInitialized;
|
||||
const parentElem = document.getElementById("lens-views");
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.name = cluster.preferences.clusterName;
|
||||
iframe.setAttribute("src", `//${clusterId}.${location.host}`)
|
||||
iframe.addEventListener("load", async () => {
|
||||
logger.info(`[LENS-VIEW]: loaded from ${iframe.src}`)
|
||||
lensViews.get(clusterId).isLoaded = true;
|
||||
})
|
||||
lensViews.set(clusterId, { clusterId, view: iframe });
|
||||
parentElem.appendChild(iframe);
|
||||
}
|
||||
|
||||
export function refreshViews() {
|
||||
const cluster = getMatchedCluster();
|
||||
lensViews.forEach(({ clusterId, view, isLoaded }) => {
|
||||
const isVisible = cluster && cluster.available && cluster.id === clusterId;
|
||||
view.style.display = isLoaded && isVisible ? "flex" : "none"
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user