mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
workspaces -- part 1
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
012ef0419f
commit
c70ed87da9
@ -41,10 +41,19 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||
return Array.from(this.workspaces.values());
|
||||
}
|
||||
|
||||
isDefault(id: WorkspaceId) {
|
||||
return id === WorkspaceStore.defaultId;
|
||||
}
|
||||
|
||||
getById(id: WorkspaceId): Workspace {
|
||||
return this.workspaces.get(id);
|
||||
}
|
||||
|
||||
@action
|
||||
setActive(id = WorkspaceStore.defaultId) {
|
||||
this.currentWorkspaceId = id;
|
||||
}
|
||||
|
||||
@action
|
||||
public saveWorkspace(workspace: Workspace) {
|
||||
const id = workspace.id;
|
||||
@ -60,11 +69,11 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||
public removeWorkspace(id: WorkspaceId) {
|
||||
const workspace = this.getById(id);
|
||||
if (!workspace) return;
|
||||
if (id === WorkspaceStore.defaultId) {
|
||||
if (this.isDefault(id)) {
|
||||
throw new Error("Cannot remove default workspace");
|
||||
}
|
||||
if (id === this.currentWorkspaceId) {
|
||||
this.currentWorkspaceId = WorkspaceStore.defaultId;
|
||||
if (this.currentWorkspaceId === id) {
|
||||
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
|
||||
}
|
||||
this.workspaces.delete(id);
|
||||
clusterStore.removeByWorkspaceId(id)
|
||||
|
||||
@ -1,32 +1,2 @@
|
||||
.AddCluster {
|
||||
--flex-gap: #{$padding * 2};
|
||||
|
||||
position: relative;
|
||||
padding: $padding * 3;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 40%;
|
||||
|
||||
> .content {
|
||||
padding: var(--flex-gap);
|
||||
margin-right: var(--flex-gap);
|
||||
background-color: var(--clusters-menu-bgc);
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
> .info-panel {
|
||||
@include hidden-scrollbar;
|
||||
padding: var(--flex-gap);
|
||||
border-left: 1px solid #353a3e;
|
||||
}
|
||||
|
||||
.error {
|
||||
border-radius: $radius;
|
||||
padding: $padding;
|
||||
background-color: pink;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $colorInfo;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import "./add-cluster.scss"
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import React from "react";
|
||||
import React, { Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { computed, observable } from "mobx";
|
||||
import { Select, SelectOption } from "../select";
|
||||
@ -17,6 +17,7 @@ import { clusterStore } from "../../../common/cluster-store";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { navigation } from "../../navigation";
|
||||
import { WizardLayout } from "../layout/wizard-layout";
|
||||
|
||||
@observer
|
||||
export class AddCluster extends React.Component {
|
||||
@ -102,107 +103,110 @@ export class AddCluster extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
renderInfo() {
|
||||
return (
|
||||
<Fragment>
|
||||
<h2>Clusters associated with Lens</h2>
|
||||
<p>
|
||||
Add clusters by clicking the <span className="text-primary">Add Cluster</span> button.
|
||||
You'll need to obtain a working kubeconfig for the cluster you want to add.
|
||||
</p>
|
||||
<p>
|
||||
Each <a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#context" target="_blank">cluster context</a> is added as a separate item in the
|
||||
left-side cluster menu
|
||||
to allow you to operate easily on multiple clusters and/or contexts.
|
||||
</p>
|
||||
<p>
|
||||
For more information on kubeconfig see <a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/" target="_blank">Kubernetes docs</a>
|
||||
</p>
|
||||
<p>
|
||||
NOTE: Any manually added cluster is not merged into your kubeconfig file.
|
||||
</p>
|
||||
<p>
|
||||
To see your currently enabled config with <code>kubectl</code>, use <code>kubectl config view --minify --raw</code> command in your terminal.
|
||||
</p>
|
||||
<p>
|
||||
When connecting to a cluster, make sure you have a valid and working kubeconfig for the cluster. Following lists known "gotchas" in some authentication types used in kubeconfig with Lens
|
||||
app.
|
||||
</p>
|
||||
<a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#option-1-oidc-authenticator" target="_blank">
|
||||
<h4>OIDC (OpenID Connect)</h4>
|
||||
</a>
|
||||
<div>
|
||||
<p>
|
||||
When connecting Lens to OIDC enabled cluster, there's few things you as a user need to take into account.
|
||||
</p>
|
||||
<b>Dedicated refresh token</b>
|
||||
<p>
|
||||
As Lens app utilized kubeconfig is "disconnected" from your main kubeconfig Lens needs to have it's own refresh token it utilizes.
|
||||
If you share the refresh token with e.g. <code>kubectl</code> who ever uses the token first will invalidate it for the next user.
|
||||
One way to achieve this is with <a href="https://github.com/int128/kubelogin" target="_blank">kubelogin</a> tool by removing the tokens
|
||||
(both <code>id_token</code> and <code>refresh_token</code>) from
|
||||
the config and issuing <code>kubelogin</code> command. That'll take you through the login process and will result you having "dedicated" refresh token.
|
||||
</p>
|
||||
</div>
|
||||
<h4>Exec auth plugins</h4>
|
||||
<p>
|
||||
When using <a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuration" target="_blank">exec auth</a> plugins make sure the paths that are used to call
|
||||
any binaries
|
||||
are full paths as Lens app might not be able to call binaries with relative paths. Make also sure that you pass all needed information either as arguments or env variables in the config,
|
||||
Lens app might not have all login shell env variables set automatically.
|
||||
</p>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="AddCluster">
|
||||
<div className="content flex column gaps">
|
||||
<h2><Trans>Add Cluster</Trans></h2>
|
||||
<Select
|
||||
placeholder={<Trans>Select kubeconfig</Trans>}
|
||||
value={this.clusterConfig}
|
||||
options={this.clusterOptions}
|
||||
onChange={({ value }: SelectOption) => this.clusterConfig = value}
|
||||
/>
|
||||
<div className="cluster-settings">
|
||||
<a href="#" onClick={() => this.showSettings = !this.showSettings}>
|
||||
<Trans>Proxy settings</Trans>
|
||||
</a>
|
||||
<WizardLayout className="AddCluster" infoPanel={this.renderInfo()}>
|
||||
<h2><Trans>Add Cluster</Trans></h2>
|
||||
<Select
|
||||
placeholder={<Trans>Select kubeconfig</Trans>}
|
||||
value={this.clusterConfig}
|
||||
options={this.clusterOptions}
|
||||
onChange={({ value }: SelectOption) => this.clusterConfig = value}
|
||||
/>
|
||||
<div className="cluster-settings">
|
||||
<a href="#" onClick={() => this.showSettings = !this.showSettings}>
|
||||
<Trans>Proxy settings</Trans>
|
||||
</a>
|
||||
</div>
|
||||
{this.showSettings && (
|
||||
<div className="proxy-settings">
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={_i18n._(t`A HTTP proxy server URL (format: http://<address>:<port>)`)}
|
||||
value={this.proxyServer}
|
||||
onChange={value => this.proxyServer = value}
|
||||
/>
|
||||
<small className="hint">
|
||||
<Trans>HTTP Proxy server. Used for communicating with Kubernetes API.</Trans>
|
||||
</small>
|
||||
</div>
|
||||
{this.showSettings && (
|
||||
<div className="proxy-settings">
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={_i18n._(t`A HTTP proxy server URL (format: http://<address>:<port>)`)}
|
||||
value={this.proxyServer}
|
||||
onChange={value => this.proxyServer = value}
|
||||
/>
|
||||
<small className="hint">
|
||||
<Trans>HTTP Proxy server. Used for communicating with Kubernetes API.</Trans>
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
{this.isCustom && (
|
||||
<div className="custom-kubeconfig flex column gaps box grow">
|
||||
<p>Kubeconfig:</p>
|
||||
<AceEditor
|
||||
autoFocus
|
||||
mode="yaml"
|
||||
value={this.customConfig}
|
||||
onChange={value => this.customConfig = value}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{this.error && (
|
||||
<div className="error">{this.error}</div>
|
||||
)}
|
||||
<div className="actions-panel">
|
||||
<Button
|
||||
primary
|
||||
label={<Trans>Add cluster</Trans>}
|
||||
onClick={this.addCluster}
|
||||
waiting={this.isWaiting}
|
||||
)}
|
||||
{this.isCustom && (
|
||||
<div className="custom-kubeconfig flex column gaps box grow">
|
||||
<p>Kubeconfig:</p>
|
||||
<AceEditor
|
||||
autoFocus
|
||||
mode="yaml"
|
||||
value={this.customConfig}
|
||||
onChange={value => this.customConfig = value}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{this.error && (
|
||||
<div className="error">{this.error}</div>
|
||||
)}
|
||||
<div className="actions-panel">
|
||||
<Button
|
||||
primary
|
||||
label={<Trans>Add cluster</Trans>}
|
||||
onClick={this.addCluster}
|
||||
waiting={this.isWaiting}
|
||||
/>
|
||||
</div>
|
||||
<div className="info-panel flex column gaps">
|
||||
<h2>Clusters associated with Lens</h2>
|
||||
<p>
|
||||
Add clusters by clicking the <span className="text-primary">Add Cluster</span> button.
|
||||
You'll need to obtain a working kubeconfig for the cluster you want to add.
|
||||
</p>
|
||||
<p>
|
||||
Each <a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#context" target="_blank">cluster context</a> is added as a separate item in the
|
||||
left-side cluster menu
|
||||
to allow you to operate easily on multiple clusters and/or contexts.
|
||||
</p>
|
||||
<p>
|
||||
For more information on kubeconfig see <a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/" target="_blank">Kubernetes docs</a>
|
||||
</p>
|
||||
<p>
|
||||
NOTE: Any manually added cluster is not merged into your kubeconfig file.
|
||||
</p>
|
||||
<p>
|
||||
To see your currently enabled config with <code>kubectl</code>, use <code>kubectl config view --minify --raw</code> command in your terminal.
|
||||
</p>
|
||||
<p>
|
||||
When connecting to a cluster, make sure you have a valid and working kubeconfig for the cluster. Following lists known "gotchas" in some authentication types used in kubeconfig with Lens
|
||||
app.
|
||||
</p>
|
||||
<a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#option-1-oidc-authenticator" target="_blank">
|
||||
<h4>OIDC (OpenID Connect)</h4>
|
||||
</a>
|
||||
<div>
|
||||
<p>
|
||||
When connecting Lens to OIDC enabled cluster, there's few things you as a user need to take into account.
|
||||
</p>
|
||||
<b>Dedicated refresh token</b>
|
||||
<p>
|
||||
As Lens app utilized kubeconfig is "disconnected" from your main kubeconfig Lens needs to have it's own refresh token it utilizes.
|
||||
If you share the refresh token with e.g. <code>kubectl</code> who ever uses the token first will invalidate it for the next user.
|
||||
One way to achieve this is with <a href="https://github.com/int128/kubelogin" target="_blank">kubelogin</a> tool by removing the tokens
|
||||
(both <code>id_token</code> and <code>refresh_token</code>) from
|
||||
the config and issuing <code>kubelogin</code> command. That'll take you through the login process and will result you having "dedicated" refresh token.
|
||||
</p>
|
||||
</div>
|
||||
<h4>Exec auth plugins</h4>
|
||||
<p>
|
||||
When using <a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuration" target="_blank">exec auth</a> plugins make sure the paths that are used to call
|
||||
any binaries
|
||||
are full paths as Lens app might not be able to call binaries with relative paths. Make also sure that you pass all needed information either as arguments or env variables in the config,
|
||||
Lens app might not have all login shell env variables set automatically.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</WizardLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
7
src/renderer/components/+workspaces/workspace-menu.scss
Normal file
7
src/renderer/components/+workspaces/workspace-menu.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.WorkspaceMenu {
|
||||
border-radius: $radius;
|
||||
|
||||
.workspaces-title {
|
||||
padding: $padding;
|
||||
}
|
||||
}
|
||||
51
src/renderer/components/+workspaces/workspace-menu.tsx
Normal file
51
src/renderer/components/+workspaces/workspace-menu.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import "./workspace-menu.scss"
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { workspacesURL } from "./workspaces.route";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { Menu, MenuItem, MenuProps } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { observable } from "mobx";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
import { cssNames } from "../../utils";
|
||||
|
||||
interface Props extends Partial<MenuProps> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class WorkspaceMenu extends React.Component<Props> {
|
||||
@observable menuVisible = false;
|
||||
|
||||
render() {
|
||||
const { className, ...menuProps } = this.props;
|
||||
const { workspacesList, currentWorkspace } = workspaceStore;
|
||||
return (
|
||||
<Menu
|
||||
{...menuProps}
|
||||
usePortal
|
||||
className={cssNames("WorkspaceMenu", className)}
|
||||
isOpen={this.menuVisible}
|
||||
open={() => this.menuVisible = true}
|
||||
close={() => this.menuVisible = false}
|
||||
>
|
||||
<Link className="workspaces-title" to={workspacesURL()}>
|
||||
<Trans>Workspaces</Trans>
|
||||
</Link>
|
||||
{workspacesList.map(({ id: workspaceId, name, description }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={workspaceId}
|
||||
title={description}
|
||||
active={workspaceId === currentWorkspace.id}
|
||||
onClick={() => workspaceStore.setActive(workspaceId)}
|
||||
>
|
||||
<Icon small material="layers"/>
|
||||
<span className="workspace">{name}</span>
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,44 @@
|
||||
import "./workspaces.scss"
|
||||
import React from "react";
|
||||
import React, { Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { WizardLayout } from "../layout/wizard-layout";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
|
||||
@observer
|
||||
export class Workspaces extends React.Component {
|
||||
render() {
|
||||
renderInfo() {
|
||||
return (
|
||||
<div className="Workspaces">
|
||||
Workspaces
|
||||
</div>
|
||||
<Fragment>
|
||||
<h2><Trans>What is a Workspace?</Trans></h2>
|
||||
<p><Trans>Workspaces are used to organize number of clusters into logical groups.</Trans></p>
|
||||
<p><Trans>A single workspaces contains a list of clusters and their full configuration.</Trans></p>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
addWorkspace = () => {
|
||||
console.log('add workspace')
|
||||
}
|
||||
|
||||
render() {
|
||||
const { workspacesList, currentWorkspace } = workspaceStore;
|
||||
return (
|
||||
<WizardLayout className="Workspaces" infoPanel={this.renderInfo()}>
|
||||
<h2>
|
||||
<Trans>Workspaces</Trans>
|
||||
</h2>
|
||||
<div className="workspaces">
|
||||
{workspacesList.map(({ id: workspaceId, name, description }) => {
|
||||
return (
|
||||
<div key={workspaceId} className="workspace flex gaps">
|
||||
<span className="name">{name}</span>
|
||||
<span className="description">{description}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</WizardLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,11 +11,3 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
#workspace-menu {
|
||||
border-radius: $radius;
|
||||
|
||||
.workspaces-title {
|
||||
padding: $padding;
|
||||
}
|
||||
}
|
||||
@ -1,56 +1,21 @@
|
||||
import "./bottom-bar.scss"
|
||||
import React from "react";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { Icon } from "../icon";
|
||||
import { Menu, MenuItem } from "../menu";
|
||||
import { WorkspaceId, workspaceStore } from "../../../common/workspace-store";
|
||||
import { workspacesURL } from "../+workspaces";
|
||||
import { WorkspaceMenu } from "../+workspaces/workspace-menu";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
|
||||
@observer
|
||||
export class BottomBar extends React.Component {
|
||||
@observable menuVisible = false;
|
||||
|
||||
selectWorkspace = (workspaceId: WorkspaceId) => {
|
||||
workspaceStore.currentWorkspaceId = workspaceId;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentWorkspace, workspacesList } = workspaceStore;
|
||||
const menuId = "workspaces-menu"
|
||||
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>
|
||||
<Menu
|
||||
usePortal
|
||||
id="workspace-menu"
|
||||
htmlFor="current-workspace"
|
||||
isOpen={this.menuVisible}
|
||||
open={() => this.menuVisible = true}
|
||||
close={() => this.menuVisible = false}
|
||||
>
|
||||
<Link className="workspaces-title" to={workspacesURL()}>
|
||||
<Trans>Workspaces</Trans>
|
||||
</Link>
|
||||
{workspacesList.map(({ id, name, description }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={id}
|
||||
active={id === currentWorkspace.id}
|
||||
onClick={() => this.selectWorkspace(id)}
|
||||
title={description}
|
||||
>
|
||||
<Icon small material="layers"/>
|
||||
<span className="workspace">{name}</span>
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
<WorkspaceMenu htmlFor="current-workspace"/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
32
src/renderer/components/layout/wizard-layout.scss
Normal file
32
src/renderer/components/layout/wizard-layout.scss
Normal file
@ -0,0 +1,32 @@
|
||||
.WizardLayout {
|
||||
--flex-gap: #{$padding * 2};
|
||||
|
||||
position: relative;
|
||||
padding: $padding * 3;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 40%;
|
||||
|
||||
> .content-col {
|
||||
padding: var(--flex-gap);
|
||||
margin-right: var(--flex-gap);
|
||||
background-color: var(--clusters-menu-bgc);
|
||||
border-radius: $radius;
|
||||
|
||||
> .error {
|
||||
border-radius: $radius;
|
||||
padding: $padding;
|
||||
background-color: pink;
|
||||
}
|
||||
}
|
||||
|
||||
> .info-col {
|
||||
@include hidden-scrollbar;
|
||||
padding: var(--flex-gap);
|
||||
border-left: 1px solid #353a3e;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $colorInfo;
|
||||
}
|
||||
}
|
||||
28
src/renderer/components/layout/wizard-layout.tsx
Normal file
28
src/renderer/components/layout/wizard-layout.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import "./wizard-layout.scss"
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
|
||||
interface Props {
|
||||
className?: IClassName;
|
||||
contentClass?: IClassName;
|
||||
infoPanelClass?: IClassName;
|
||||
infoPanel?: React.ReactNode;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class WizardLayout extends React.Component<Props> {
|
||||
render() {
|
||||
const { className, contentClass, infoPanelClass, infoPanel, children: content } = this.props;
|
||||
return (
|
||||
<div className={cssNames("WizardLayout", className)}>
|
||||
<div className={cssNames("content-col flex column gaps", contentClass)}>
|
||||
{content}
|
||||
</div>
|
||||
<div className={cssNames("info-col flex column gaps", infoPanelClass)}>
|
||||
{infoPanel}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user