1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into fix/empty_cluster_view

This commit is contained in:
Roman 2020-09-22 14:19:14 +03:00
commit ac7b399c81
36 changed files with 311 additions and 201 deletions

View File

@ -17,10 +17,6 @@ describe("app start", () => {
const addMinikubeCluster = async (app: Application) => {
await app.client.click("div.add-cluster")
await app.client.waitUntilTextExists("div", "Select kubeconfig file")
await app.client.click("div.Select__control")
await app.client.waitUntilTextExists("div", "minikube")
await app.client.click("div.minikube")
await app.client.click("div.Select__control")
await app.client.click("button.primary")
}

View File

@ -2,7 +2,7 @@
"name": "kontena-lens",
"productName": "Lens",
"description": "Lens - The Kubernetes IDE",
"version": "3.6.3",
"version": "3.6.4",
"main": "static/build/main.js",
"copyright": "© 2020, Mirantis, Inc.",
"license": "MIT",

View File

@ -14,6 +14,14 @@ export const clusterIpc = {
},
}),
refresh: createIpcChannel({
channel: "cluster:refresh",
handle: (clusterId: ClusterId) => {
const cluster = clusterStore.getById(clusterId);
if (cluster) return cluster.refresh();
},
}),
disconnect: createIpcChannel({
channel: "cluster:disconnect",
handle: (clusterId: ClusterId) => {

View File

@ -133,7 +133,13 @@ export class Cluster implements ClusterModel {
if (this.disconnected || (!init && !this.accessible)) {
await this.reconnect();
}
await this.refresh();
await this.refreshConnectionStatus()
if (this.accessible) {
await this.refreshAllowedResources()
this.ready = true
this.kubeCtl = new Kubectl(this.version)
this.kubeCtl.ensureKubectl() // download kubectl in background, so it's not blocking dashboard
}
return this.pushState();
}
@ -159,15 +165,14 @@ export class Cluster implements ClusterModel {
@action
async refresh() {
logger.info(`[CLUSTER]: refresh`, this.getMeta());
await this.refreshConnectionStatus(); // refresh "version", "online", etc.
await this.whenInitialized;
await this.refreshConnectionStatus();
if (this.accessible) {
this.kubeCtl = new Kubectl(this.version)
this.distribution = this.detectKubernetesDistribution(this.version)
const [features, isAdmin, nodesCount] = await Promise.all([
getFeatures(this),
this.isClusterAdmin(),
this.getNodeCount(),
this.kubeCtl.ensureKubectl()
]);
this.features = features;
this.isAdmin = isAdmin;
@ -176,8 +181,8 @@ export class Cluster implements ClusterModel {
this.refreshEvents(),
this.refreshAllowedResources(),
]);
this.ready = true
}
this.pushState();
}
@action

View File

@ -39,7 +39,7 @@ export class ShellSession extends EventEmitter {
public async open() {
this.kubectlBinDir = await this.kubectl.binDir()
const pathFromPreferences = userStore.preferences.kubectlBinariesPath || Kubectl.bundledKubectlPath
this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? await this.kubectl.binDir() : path.dirname(pathFromPreferences)
this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences)
this.helmBinDir = helmCli.getBinaryDir()
const env = await this.getCachedShellEnv()
const shell = env.PTYSHELL

View File

@ -1,15 +1,15 @@
import React from "react";
import { Trans } from "@lingui/macro";
import { MainLayout } from "../layout/main-layout";
import { TabLayout } from "../layout/tab-layout";
export class NotFound extends React.Component {
render() {
return (
<MainLayout className="NotFound" contentClass="flex" footer={null}>
<TabLayout className="NotFound" contentClass="flex">
<p className="box center">
<Trans>Page not found</Trans>
</p>
</MainLayout>
)
</TabLayout>
);
}
}
}

View File

@ -80,7 +80,6 @@ export class AddCluster extends React.Component {
const contexts = this.getContexts(this.kubeConfigLocal);
this.kubeContexts.replace(contexts);
break;
case KubeConfigSourceTab.TEXT:
try {
this.error = ""
@ -91,6 +90,10 @@ export class AddCluster extends React.Component {
}
break;
}
if (this.kubeContexts.size === 1) {
this.selectedContexts.push(this.kubeContexts.keys().next().value)
}
}
getContexts(config: KubeConfig): Map<string, KubeConfig> {
@ -206,7 +209,7 @@ export class AddCluster extends React.Component {
<Tab
value={KubeConfigSourceTab.FILE}
label={<Trans>Select kubeconfig file</Trans>}
active={this.sourceTab == KubeConfigSourceTab.FILE}/>
active={this.sourceTab == KubeConfigSourceTab.FILE} />
<Tab
value={KubeConfigSourceTab.TEXT}
label={<Trans>Paste as text</Trans>}
@ -320,13 +323,15 @@ export class AddCluster extends React.Component {
return (
<div className={cssNames("kube-context flex gaps align-center", context)}>
<span>{context}</span>
{isNew && <Icon small material="fiber_new"/>}
{isSelected && <Icon small material="check" className="box right"/>}
{isNew && <Icon small material="fiber_new" />}
{isSelected && <Icon small material="check" className="box right" />}
</div>
)
};
render() {
const addDisabled = this.selectedContexts.length === 0
return (
<WizardLayout
className="AddCluster"
@ -374,9 +379,12 @@ export class AddCluster extends React.Component {
<div className="actions-panel">
<Button
primary
disabled={addDisabled}
label={<Trans>Add cluster(s)</Trans>}
onClick={this.addClusters}
waiting={this.isWaiting}
tooltip={addDisabled ? _i18n._("Select at least one cluster to add.") : undefined}
tooltipOverrideDisabled
/>
</div>
</WizardLayout>

View File

@ -2,7 +2,7 @@ import React from "react";
import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts";
import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases";
import { namespaceStore } from "../+namespaces/namespace.store";
@ -30,12 +30,12 @@ export class Apps extends React.Component {
render() {
const tabRoutes = Apps.tabRoutes;
return (
<MainLayout className="Apps" tabs={tabRoutes}>
<TabLayout className="Apps" tabs={tabRoutes}>
<Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
<Redirect to={tabRoutes[0].url}/>
</Switch>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -1,11 +1,12 @@
import "./cluster-settings.scss";
import React from "react";
import { observer } from "mobx-react";
import { observer, disposeOnUnmount } from "mobx-react";
import { Features } from "./features";
import { Removal } from "./removal";
import { Status } from "./status";
import { General } from "./general";
import { Cluster } from "../../../main/cluster";
import { WizardLayout } from "../layout/wizard-layout";
import { ClusterIcon } from "../cluster-icon";
import { Icon } from "../icon";
@ -13,14 +14,25 @@ import { navigate } from "../../navigation";
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
import { clusterStore } from "../../../common/cluster-store";
import { RouteComponentProps } from "react-router";
import { clusterIpc } from "../../../common/cluster-ipc";
import { autorun } from "mobx";
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
}
@observer
export class ClusterSettings extends React.Component<Props> {
get cluster(): Cluster {
return clusterStore.getById(this.props.match.params.clusterId);
}
async componentDidMount() {
window.addEventListener('keydown', this.onEscapeKey);
disposeOnUnmount(this,
autorun(() => {
this.refreshCluster();
})
)
}
componentWillUnmount() {
@ -34,12 +46,18 @@ export class ClusterSettings extends React.Component<Props> {
}
}
refreshCluster = () => {
if(this.cluster) {
clusterIpc.refresh.invokeFromRenderer(this.cluster.id);
}
}
close() {
navigate("/");
}
render() {
const cluster = clusterStore.getById(this.props.match.params.clusterId);
const cluster = this.cluster
if (!cluster) return null;
const header = (
<>

View File

@ -3,7 +3,7 @@ import "./cluster.scss"
import React from "react";
import { computed, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { MainLayout } from "../layout/main-layout";
import { TabLayout } from "../layout/tab-layout";
import { ClusterIssues } from "./cluster-issues";
import { Spinner } from "../spinner";
import { cssNames, interval, isElectron } from "../../utils";
@ -54,7 +54,7 @@ export class Cluster extends React.Component {
render() {
const { isLoaded } = this;
return (
<MainLayout>
<TabLayout>
<div className="Cluster">
{!isLoaded && <Spinner center/>}
{isLoaded && (
@ -65,7 +65,7 @@ export class Cluster extends React.Component {
</>
)}
</div>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -2,7 +2,7 @@ import React from "react";
import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { ConfigMaps, configMapsRoute, configMapsURL } from "../+config-maps";
import { Secrets, secretsRoute, secretsURL } from "../+config-secrets";
import { namespaceStore } from "../+namespaces/namespace.store";
@ -68,12 +68,12 @@ export class Config extends React.Component {
render() {
const tabRoutes = Config.tabRoutes;
return (
<MainLayout className="Config" tabs={tabRoutes}>
<TabLayout className="Config" tabs={tabRoutes}>
<Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
<Redirect to={configURL({ query: namespaceStore.getContextParams() })}/>
</Switch>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -2,7 +2,7 @@ import React from "react";
import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { crdResourcesRoute, crdRoute, crdURL, crdDefinitionsRoute } from "./crd.route";
import { CrdList } from "./crd-list";
import { CrdResources } from "./crd-resources";
@ -25,13 +25,13 @@ export class CustomResources extends React.Component {
render() {
return (
<MainLayout>
<TabLayout>
<Switch>
<Route component={CrdList} {...crdDefinitionsRoute} exact/>
<Route component={CrdResources} {...crdResourcesRoute}/>
<Redirect to={crdURL()}/>
</Switch>
</MainLayout>
</TabLayout>
);
}
}

View File

@ -2,7 +2,7 @@ import "./events.scss";
import React, { Fragment } from "react";
import { observer } from "mobx-react";
import { MainLayout } from "../layout/main-layout";
import { TabLayout } from "../layout/tab-layout";
import { eventStore } from "./event.store";
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
import { Trans } from "@lingui/macro";
@ -118,9 +118,9 @@ export class Events extends React.Component<Props> {
return events;
}
return (
<MainLayout>
<TabLayout>
{events}
</MainLayout>
</TabLayout>
)
}
}

View File

@ -87,6 +87,11 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
super.reset();
this.contextNs.clear();
}
async remove(item: Namespace) {
await super.remove(item);
this.contextNs.remove(item.getName());
}
}
export const namespaceStore = new NamespaceStore();

View File

@ -4,7 +4,7 @@ import React from "react";
import { Trans } from "@lingui/macro";
import { Namespace, namespacesApi, NamespaceStatus } from "../../api/endpoints";
import { AddNamespaceDialog } from "./add-namespace-dialog";
import { MainLayout } from "../layout/main-layout";
import { TabLayout } from "../layout/tab-layout";
import { Badge } from "../badge";
import { RouteComponentProps } from "react-router";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
@ -26,7 +26,7 @@ interface Props extends RouteComponentProps<INamespacesRouteParams> {
export class Namespaces extends React.Component<Props> {
render() {
return (
<MainLayout>
<TabLayout>
<KubeObjectListLayout
isClusterScoped
className="Namespaces" store={namespaceStore}
@ -65,7 +65,7 @@ export class Namespaces extends React.Component<Props> {
})}
/>
<AddNamespaceDialog/>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { RouteComponentProps } from "react-router-dom";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { Services, servicesRoute, servicesURL } from "../+network-services";
import { Endpoints, endpointRoute, endpointURL } from "../+network-endpoints";
import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses";
@ -60,12 +60,12 @@ export class Network extends React.Component<Props> {
render() {
const tabRoutes = Network.tabRoutes;
return (
<MainLayout className="Network" tabs={tabRoutes}>
<TabLayout className="Network" tabs={tabRoutes}>
<Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
<Redirect to={networkURL({ query: namespaceStore.getContextParams() })}/>
</Switch>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import { RouteComponentProps } from "react-router";
import { t, Trans } from "@lingui/macro";
import { cssNames, interval } from "../../utils";
import { MainLayout } from "../layout/main-layout";
import { TabLayout } from "../layout/tab-layout";
import { nodesStore } from "./nodes.store";
import { podsStore } from "../+workloads-pods/pods.store";
import { KubeObjectListLayout } from "../kube-object";
@ -123,7 +123,7 @@ export class Nodes extends React.Component<Props> {
render() {
return (
<MainLayout>
<TabLayout>
<KubeObjectListLayout
className="Nodes"
store={nodesStore} isClusterScoped
@ -182,7 +182,7 @@ export class Nodes extends React.Component<Props> {
return <NodeMenu object={item}/>
}}
/>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { RouteComponentProps } from "react-router-dom";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
@ -52,12 +52,12 @@ export class Storage extends React.Component<Props> {
render() {
const tabRoutes = Storage.tabRoutes;
return (
<MainLayout className="Storage" tabs={tabRoutes}>
<TabLayout className="Storage" tabs={tabRoutes}>
<Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
<Redirect to={storageURL({ query: namespaceStore.getContextParams() })}/>
</Switch>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { RouteComponentProps } from "react-router-dom";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { Roles } from "../+user-management-roles";
import { RoleBindings } from "../+user-management-roles-bindings";
import { ServiceAccounts } from "../+user-management-service-accounts";
@ -55,12 +55,12 @@ export class UserManagement extends React.Component<Props> {
render() {
const tabRoutes = UserManagement.tabRoutes;
return (
<MainLayout className="UserManagement" tabs={tabRoutes}>
<TabLayout className="UserManagement" tabs={tabRoutes}>
<Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
<Redirect to={usersManagementURL({ query: namespaceStore.getContextParams() })}/>
</Switch>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -1,10 +1,24 @@
.WhatsNew {
$spacing: $padding * 2;
background: $mainBackground url(../../components/icon/crane.svg) no-repeat;
background-position: 0 35%;
background-size: 85%;
background-clip: content-box;
&::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;
}
}
.logo {
width: 200px;

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { RouteComponentProps } from "react-router-dom";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { WorkloadsOverview } from "../+workloads-overview/overview";
import { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, statefulSetsRoute, statefulSetsURL, workloadsURL } from "./workloads.route";
import { namespaceStore } from "../+namespaces/namespace.store";
@ -86,12 +86,12 @@ export class Workloads extends React.Component<Props> {
render() {
const tabRoutes = Workloads.tabRoutes;
return (
<MainLayout className="Workloads" tabs={tabRoutes}>
<TabLayout className="Workloads" tabs={tabRoutes}>
<Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
<Redirect to={workloadsURL({ query: namespaceStore.getContextParams() })}/>
</Switch>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -35,6 +35,7 @@ import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store
import logger from "../../main/logger";
import { clusterIpc } from "../../common/cluster-ipc";
import { webFrame } from "electron";
import { MainLayout } from "./layout/main-layout";
@observer
export class App extends React.Component {
@ -59,21 +60,23 @@ export class App extends React.Component {
<I18nProvider i18n={_i18n}>
<Router history={history}>
<ErrorBoundary>
<Switch>
<Route component={Cluster} {...clusterRoute}/>
<Route component={Nodes} {...nodesRoute}/>
<Route component={Workloads} {...workloadsRoute}/>
<Route component={Config} {...configRoute}/>
<Route component={Network} {...networkRoute}/>
<Route component={Storage} {...storageRoute}/>
<Route component={Namespaces} {...namespacesRoute}/>
<Route component={Events} {...eventRoute}/>
<Route component={CustomResources} {...crdRoute}/>
<Route component={UserManagement} {...usersManagementRoute}/>
<Route component={Apps} {...appsRoute}/>
<Redirect exact from="/" to={this.startURL}/>
<Route component={NotFound}/>
</Switch>
<MainLayout>
<Switch>
<Route component={Cluster} {...clusterRoute}/>
<Route component={Nodes} {...nodesRoute}/>
<Route component={Workloads} {...workloadsRoute}/>
<Route component={Config} {...configRoute}/>
<Route component={Network} {...networkRoute}/>
<Route component={Storage} {...storageRoute}/>
<Route component={Namespaces} {...namespacesRoute}/>
<Route component={Events} {...eventRoute}/>
<Route component={CustomResources} {...crdRoute}/>
<Route component={UserManagement} {...usersManagementRoute}/>
<Route component={Apps} {...appsRoute}/>
<Redirect exact from="/" to={this.startURL}/>
<Route component={NotFound}/>
</Switch>
</MainLayout>
<Notifications/>
<ConfirmDialog/>
<KubeObjectDetails/>

View File

@ -54,8 +54,7 @@
form:not([novalidate]):invalid &[type=submit]:not(.active),
&:disabled {
color: silver;
background: $buttonDisabledBackground;
opacity: 50%;
pointer-events: none;
}
@ -112,4 +111,4 @@
left: 100%;
width: 0;
}
}
}

View File

@ -39,7 +39,7 @@ export class ClusterStatus extends React.Component<Props> {
});
})
if (this.cluster.disconnected) {
await this.refreshCluster();
await this.activateCluster();
}
}
@ -47,13 +47,13 @@ export class ClusterStatus extends React.Component<Props> {
ipcRenderer.removeAllListeners(`kube-auth:${this.props.clusterId}`);
}
refreshCluster = async () => {
activateCluster = async () => {
await clusterIpc.activate.invokeFromRenderer(this.props.clusterId);
}
reconnect = async () => {
this.isReconnecting = true;
await this.refreshCluster();
await this.activateCluster();
this.isReconnecting = false;
}

View File

@ -48,6 +48,7 @@ export class EditorPanel extends React.Component<Props> {
onResize = () => {
this.editor.resize();
this.editor.focus();
}
onCursorPosChange = (pos: Ace.Point) => {

View File

@ -62,14 +62,13 @@ export class InfoPanel extends Component<Props> {
this.error = "";
this.waiting = true;
try {
this.result = await this.props.submit().finally(() => {
this.waiting = false;
});
this.result = await this.props.submit()
if (showNotifications) Notifications.ok(this.result);
} catch (error) {
this.error = error.toString();
if (showNotifications) Notifications.error(this.error);
throw error;
} finally {
this.waiting = false
}
}
@ -91,12 +90,13 @@ export class InfoPanel extends Component<Props> {
<>
{result && (
<div className="success flex align-center">
<Icon material="done"/> <span>{result}</span>
<Icon material="done" />
<span>{result}</span>
</div>
)}
{errorInfo && (
<div className="error flex align-center">
<Icon material="error_outline"/>
<Icon material="error_outline" />
<span>{errorInfo}</span>
</div>
)}
@ -114,9 +114,9 @@ export class InfoPanel extends Component<Props> {
{controls}
</div>
<div className="info flex gaps align-center">
{waiting ? <><Spinner/> {submittingMessage}</> : this.renderInfo()}
{waiting ? <><Spinner /> {submittingMessage}</> : this.renderInfo()}
</div>
<Button plain label={<Trans>Cancel</Trans>} onClick={close}/>
<Button plain label={<Trans>Cancel</Trans>} onClick={close} />
<Button
primary active
label={submitLabel}
@ -134,4 +134,4 @@ export class InfoPanel extends Component<Props> {
</div>
);
}
}
}

View File

@ -152,6 +152,7 @@ export class Terminal {
onResize = () => {
if (!this.isActive) return;
this.fitLazy();
this.focus();
}
onActivate = () => {

View File

@ -1,19 +1,18 @@
.MainLayout {
--sidebar-max-size: 200px;
display: grid;
grid-template-areas: "aside header" "aside tabs" "aside main" "aside footer";
grid-template-areas:
"aside header"
"aside tabs"
"aside main"
"aside footer";
grid-template-rows: [header] var(--main-layout-header) [tabs] min-content [main] 1fr [footer] auto;
grid-template-columns: [sidebar] minmax(var(--main-layout-header), min-content) [main] 1fr;
height: 100%;
> .Tabs {
grid-area: tabs;
background: $layoutTabsBackground;
}
header {
> header {
grid-area: header;
background: $layoutBackground;
padding: $padding $padding * 2;
@ -28,7 +27,7 @@
}
}
aside {
> aside {
grid-area: aside;
position: relative;
background: $sidebarBackground;
@ -48,25 +47,14 @@
&.accessible:hover {
width: var(--sidebar-max-size);
transition-delay: 750ms;
box-shadow: 3px 3px 16px rgba(0, 0, 0, .35);
box-shadow: 3px 3px 16px rgba(0, 0, 0, 0.35);
z-index: $zIndex-sidebar-hover;
}
}
}
main {
@include custom-scrollbar;
$spacing: $margin * 2;
.theme-light & {
@include custom-scrollbar(dark);
}
grid-area: main;
overflow-y: scroll; // always reserve space for scrollbar (17px)
overflow-x: auto;
margin: $spacing;
margin-right: 0;
> main {
display: contents;
}
footer {
@ -74,4 +62,4 @@
grid-area: footer;
min-width: 0; // restrict size when overflow content (e.g. <Dock> tabs scrolling)
}
}
}

View File

@ -3,26 +3,16 @@ import "./main-layout.scss";
import React from "react";
import { observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { matchPath, RouteProps } from "react-router-dom";
import { createStorage, cssNames } from "../../utils";
import { Tab, Tabs } from "../tabs";
import { Sidebar } from "./sidebar";
import { ErrorBoundary } from "../error-boundary";
import { Dock } from "../dock";
import { navigate, navigation } from "../../navigation";
import { getHostedCluster } from "../../../common/cluster-store";
export interface TabRoute extends RouteProps {
title: React.ReactNode;
url: string;
}
interface Props {
className?: any;
tabs?: TabRoute[];
footer?: React.ReactNode;
headerClass?: string;
contentClass?: string;
footerClass?: string;
}
@ -35,18 +25,17 @@ export class MainLayout extends React.Component<Props> {
@disposeOnUnmount syncPinnedStateWithStorage = reaction(
() => this.isPinned,
isPinned => this.storage.merge({ pinnedSidebar: isPinned })
(isPinned) => this.storage.merge({ pinnedSidebar: isPinned })
);
toggleSidebar = () => {
this.isPinned = !this.isPinned;
this.isAccessible = false;
setTimeout(() => this.isAccessible = true, 250);
}
setTimeout(() => (this.isAccessible = true), 250);
};
render() {
const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props;
const routePath = navigation.location.pathname;
const { className, headerClass, footer, footerClass, children } = this.props;
const cluster = getHostedCluster();
if (!cluster) {
return null; // fix: skip render when removing active (visible) cluster
@ -54,37 +43,18 @@ export class MainLayout extends React.Component<Props> {
return (
<div className={cssNames("MainLayout", className)}>
<header className={cssNames("flex gaps align-center", headerClass)}>
<span className="cluster">
{cluster.preferences.clusterName || cluster.contextName}
</span>
<span className="cluster">{cluster.preferences.clusterName || cluster.contextName}</span>
</header>
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>
<Sidebar
className="box grow"
isPinned={this.isPinned}
toggle={this.toggleSidebar}
/>
<Sidebar className="box grow" isPinned={this.isPinned} toggle={this.toggleSidebar} />
</aside>
{tabs && (
<Tabs center onChange={url => navigate(url)}>
{tabs.map(({ title, path, url, ...routeProps }) => {
const isActive = !!matchPath(routePath, { path, ...routeProps });
return <Tab key={url} label={title} value={url} active={isActive}/>
})}
</Tabs>
)}
<main className={contentClass}>
<ErrorBoundary>
{children}
</ErrorBoundary>
<main>
<ErrorBoundary>{children}</ErrorBoundary>
</main>
<footer className={footerClass}>
{footer === undefined ? <Dock/> : footer}
</footer>
<footer className={footerClass}>{footer === undefined ? <Dock /> : footer}</footer>
</div>
);
}

View File

@ -1,4 +1,4 @@
import type { TabRoute } from "./main-layout";
import type { TabRoute } from "./tab-layout";
import "./sidebar.scss";
import React from "react";
@ -27,7 +27,7 @@ import { crdStore } from "../+custom-resources/crd.store";
import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resources";
import { CustomResources } from "../+custom-resources/custom-resources";
import { navigation } from "../../navigation";
import { isAllowedResource } from "../../../common/rbac"
import { isAllowedResource } from "../../../common/rbac";
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
type SidebarContextValue = {
@ -43,21 +43,21 @@ interface Props {
@observer
export class Sidebar extends React.Component<Props> {
async componentDidMount() {
if (!crdStore.isLoaded && isAllowedResource('customresourcedefinitions')) {
crdStore.loadAll()
if (!crdStore.isLoaded && isAllowedResource("customresourcedefinitions")) {
crdStore.loadAll();
}
}
renderCustomResources() {
return Object.entries(crdStore.groups).map(([group, crds]) => {
const submenus = crds.map(crd => {
const submenus = crds.map((crd) => {
return {
title: crd.getResourceKind(),
component: CrdList,
url: crd.getResourceUrl(),
path: crdResourcesRoute.path,
}
})
};
});
return (
<SidebarNavItem
key={group}
@ -67,8 +67,8 @@ export class Sidebar extends React.Component<Props> {
subMenus={submenus}
text={group}
/>
)
})
);
});
}
render() {
@ -79,7 +79,7 @@ export class Sidebar extends React.Component<Props> {
<div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}>
<div className="header flex align-center">
<NavLink exact to="/" className="box grow">
<Icon svg="logo-full" className="logo-icon"/>
<Icon svg="logo-full" className="logo-icon" />
<div className="logo-text">Lens</div>
</NavLink>
<Icon
@ -93,17 +93,17 @@ export class Sidebar extends React.Component<Props> {
<div className="sidebar-nav flex column box grow-fixed">
<SidebarNavItem
id="cluster"
isHidden={!isAllowedResource('nodes')}
isHidden={!isAllowedResource("nodes")}
url={clusterURL()}
text={<Trans>Cluster</Trans>}
icon={<Icon svg="kube"/>}
icon={<Icon svg="kube" />}
/>
<SidebarNavItem
id="nodes"
isHidden={!isAllowedResource('nodes')}
isHidden={!isAllowedResource("nodes")}
url={nodesURL()}
text={<Trans>Nodes</Trans>}
icon={<Icon svg="nodes"/>}
icon={<Icon svg="nodes" />}
/>
<SidebarNavItem
id="workloads"
@ -112,7 +112,7 @@ export class Sidebar extends React.Component<Props> {
routePath={workloadsRoute.path}
subMenus={Workloads.tabRoutes}
text={<Trans>Workloads</Trans>}
icon={<Icon svg="workloads"/>}
icon={<Icon svg="workloads" />}
/>
<SidebarNavItem
id="config"
@ -121,7 +121,7 @@ export class Sidebar extends React.Component<Props> {
routePath={configRoute.path}
subMenus={Config.tabRoutes}
text={<Trans>Configuration</Trans>}
icon={<Icon material="list"/>}
icon={<Icon material="list" />}
/>
<SidebarNavItem
id="networks"
@ -130,7 +130,7 @@ export class Sidebar extends React.Component<Props> {
routePath={networkRoute.path}
subMenus={Network.tabRoutes}
text={<Trans>Network</Trans>}
icon={<Icon material="device_hub"/>}
icon={<Icon material="device_hub" />}
/>
<SidebarNavItem
id="storage"
@ -138,22 +138,22 @@ export class Sidebar extends React.Component<Props> {
url={storageURL({ query })}
routePath={storageRoute.path}
subMenus={Storage.tabRoutes}
icon={<Icon svg="storage"/>}
icon={<Icon svg="storage" />}
text={<Trans>Storage</Trans>}
/>
<SidebarNavItem
id="namespaces"
isHidden={!isAllowedResource('namespaces')}
isHidden={!isAllowedResource("namespaces")}
url={namespacesURL()}
icon={<Icon material="layers"/>}
icon={<Icon material="layers" />}
text={<Trans>Namespaces</Trans>}
/>
<SidebarNavItem
id="events"
isHidden={!isAllowedResource('events')}
isHidden={!isAllowedResource("events")}
url={eventsURL({ query })}
routePath={eventRoute.path}
icon={<Icon material="access_time"/>}
icon={<Icon material="access_time" />}
text={<Trans>Events</Trans>}
/>
<SidebarNavItem
@ -161,7 +161,7 @@ export class Sidebar extends React.Component<Props> {
url={appsURL({ query })}
subMenus={Apps.tabRoutes}
routePath={appsRoute.path}
icon={<Icon material="apps"/>}
icon={<Icon material="apps" />}
text={<Trans>Apps</Trans>}
/>
<SidebarNavItem
@ -169,16 +169,16 @@ export class Sidebar extends React.Component<Props> {
url={usersManagementURL({ query })}
routePath={usersManagementRoute.path}
subMenus={UserManagement.tabRoutes}
icon={<Icon material="security"/>}
icon={<Icon material="security" />}
text={<Trans>Access Control</Trans>}
/>
<SidebarNavItem
id="custom-resources"
isHidden={!isAllowedResource('customresourcedefinitions')}
isHidden={!isAllowedResource("customresourcedefinitions")}
url={crdURL()}
subMenus={CustomResources.tabRoutes}
routePath={crdRoute.path}
icon={<Icon material="extension"/>}
icon={<Icon material="extension" />}
text={<Trans>Custom Resources</Trans>}
>
{this.renderCustomResources()}
@ -186,7 +186,7 @@ export class Sidebar extends React.Component<Props> {
</div>
</div>
</SidebarContext.Provider>
)
);
}
}
@ -203,7 +203,10 @@ interface SidebarNavItemProps {
const navItemStorage = createStorage<[string, boolean][]>("sidebar_menu_item", []);
const navItemState = observable.map<string, boolean>(navItemStorage.get());
reaction(() => [...navItemState], value => navItemStorage.set(value));
reaction(
() => [...navItemState],
(value) => navItemStorage.set(value)
);
@observer
class SidebarNavItem extends React.Component<SidebarNavItemProps> {
@ -216,15 +219,15 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
toggleSubMenu = () => {
navItemState.set(this.props.id, !this.isExpanded);
}
};
isActive = () => {
const { routePath, url } = this.props;
const { pathname } = navigation.location;
return !!matchPath(pathname, {
path: routePath || url
path: routePath || url,
});
}
};
render() {
const { id, isHidden, subMenus = [], icon, text, url, children, className } = this.props;
@ -239,10 +242,7 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
<div className={cssNames("nav-item", { active: isActive })} onClick={this.toggleSubMenu}>
{icon}
<span className="link-text">{text}</span>
<Icon
className="expand-icon"
material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"}
/>
<Icon className="expand-icon" material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"} />
</div>
<ul className={cssNames("sub-menu", { active: isActive })}>
{subMenus.map(({ title, url }) => (
@ -252,18 +252,18 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
))}
{React.Children.toArray(children).map((child: React.ReactElement<any>) => {
return React.cloneElement(child, {
className: cssNames(child.props.className, { visible: this.isExpanded })
className: cssNames(child.props.className, { visible: this.isExpanded }),
});
})}
</ul>
</div>
)
);
}
return (
<NavLink className={cssNames("SidebarNavItem", className)} to={url} isActive={this.isActive}>
{icon}
<span className="link-text">{text}</span>
</NavLink>
)
);
}
}

View File

@ -0,0 +1,25 @@
.TabLayout {
display: contents;
> .Tabs {
grid-area: tabs;
background: $layoutTabsBackground;
}
main {
@include custom-scrollbar;
$spacing: $margin * 2;
.theme-light & {
@include custom-scrollbar(dark);
}
grid-area: main;
overflow-y: scroll; // always reserve space for scrollbar (17px)
overflow-x: auto;
margin: $spacing;
margin-right: 0;
}
}

View File

@ -0,0 +1,45 @@
import "./tab-layout.scss";
import React, { ReactNode } from "react";
import { matchPath, RouteProps } from "react-router-dom";
import { observer } from "mobx-react";
import { cssNames } from "../../utils";
import { Tab, Tabs } from "../tabs";
import { ErrorBoundary } from "../error-boundary";
import { navigate, navigation } from "../../navigation";
import { getHostedCluster } from "../../../common/cluster-store";
export interface TabRoute extends RouteProps {
title: React.ReactNode;
url: string;
}
interface Props {
children: ReactNode;
className?: any;
tabs?: TabRoute[];
contentClass?: string;
}
export const TabLayout = observer(({ className, contentClass, tabs, children }: Props) => {
const routePath = navigation.location.pathname;
const cluster = getHostedCluster();
if (!cluster) {
return null; // fix: skip render when removing active (visible) cluster
}
return (
<div className={cssNames("TabLayout", className)}>
{tabs && (
<Tabs center onChange={(url) => navigate(url)}>
{tabs.map(({ title, path, url, ...routeProps }) => {
const isActive = !!matchPath(routePath, { path, ...routeProps });
return <Tab key={url} label={title} value={url} active={isActive} />;
})}
</Tabs>
)}
<main className={contentClass}>
<ErrorBoundary>{children}</ErrorBoundary>
</main>
</div>
);
});

View File

@ -3,10 +3,11 @@
position: absolute;
right: 0;
bottom: 0;
top: 0;
padding: $padding * 2;
max-height: 100vh;
z-index: 100000;
height: min-content!important;
&:empty {
display: none;
@ -42,4 +43,4 @@
}
}
}
}
}

View File

@ -15,6 +15,7 @@ export enum TooltipPosition {
export interface TooltipProps {
targetId: string; // html-id of target element to bind for
tooltipOnParentHover?: boolean; // detect hover on parent of target
visible?: boolean; // initial visibility
offset?: number; // offset from target element in pixels (all sides)
usePortal?: boolean; // renders element outside of parent (in body), disable for "easy-styling", default: true
@ -50,14 +51,22 @@ export class Tooltip extends React.Component<TooltipProps> {
return document.getElementById(this.props.targetId)
}
get hoverTarget(): HTMLElement {
if (this.props.tooltipOnParentHover) {
return this.targetElem.parentElement
}
return this.targetElem
}
componentDidMount() {
this.targetElem.addEventListener("mouseenter", this.onEnterTarget)
this.targetElem.addEventListener("mouseleave", this.onLeaveTarget)
this.hoverTarget.addEventListener("mouseenter", this.onEnterTarget)
this.hoverTarget.addEventListener("mouseleave", this.onLeaveTarget)
}
componentWillUnmount() {
this.targetElem.removeEventListener("mouseenter", this.onEnterTarget)
this.targetElem.removeEventListener("mouseleave", this.onLeaveTarget)
this.hoverTarget.removeEventListener("mouseenter", this.onEnterTarget)
this.hoverTarget.removeEventListener("mouseleave", this.onLeaveTarget)
}
@autobind()

View File

@ -8,6 +8,11 @@ import uniqueId from "lodash/uniqueId"
export interface TooltipDecoratorProps {
tooltip?: ReactNode | Omit<TooltipProps, "targetId">;
/**
* forces tooltip to detect the target's parent for mouse events. This is
* useful for displaying tooltips even when the target is "disabled"
*/
tooltipOverrideDisabled?: boolean;
}
export function withTooltip<T extends React.ComponentType<any>>(Target: T): T {
@ -17,22 +22,25 @@ export function withTooltip<T extends React.ComponentType<any>>(Target: T): T {
protected tooltipId = uniqueId("tooltip_target_");
render() {
const { tooltip, ...targetProps } = this.props;
const { tooltip, tooltipOverrideDisabled, ...targetProps } = this.props;
if (tooltip) {
const tooltipId = targetProps.id || this.tooltipId;
const tooltipProps: TooltipProps = {
targetId: tooltipId,
tooltipOnParentHover: tooltipOverrideDisabled,
...(isReactNode(tooltip) ? { children: tooltip } : tooltip),
};
targetProps.id = tooltipId;
targetProps.children = (
<>
{targetProps.children}
<Tooltip {...tooltipProps}/>
<div>
{targetProps.children}
</div>
<Tooltip {...tooltipProps} />
</>
)
}
return <Target {...targetProps as any}/>;
return <Target {...targetProps as any} />;
}
}

View File

@ -2,7 +2,13 @@
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
## 3.6.3 (current version)
## 3.6.4 (current version)
- Fix: deleted namespace does not get auto unselected
- Get focus to dock tab (terminal & resource editor) content after resize
- Downloading kubectl binary does not block dashboard opening anymore
- Fix background image of What's New page on white theme
## 3.6.3
- Fix app crash on certain situations when opening ingress details
- Reduce app minimum size to support >= 800 x 600 resolution displays
- Fix app crash when service account has imagePullSecrets defined but the actual secret is missing