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:
commit
ac7b399c81
@ -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")
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = (
|
||||
<>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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/>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -48,6 +48,7 @@ export class EditorPanel extends React.Component<Props> {
|
||||
|
||||
onResize = () => {
|
||||
this.editor.resize();
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
onCursorPosChange = (pos: Ace.Point) => {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,6 +152,7 @@ export class Terminal {
|
||||
onResize = () => {
|
||||
if (!this.isActive) return;
|
||||
this.fitLazy();
|
||||
this.focus();
|
||||
}
|
||||
|
||||
onActivate = () => {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
25
src/renderer/components/layout/tab-layout.scss
Executable file
25
src/renderer/components/layout/tab-layout.scss
Executable 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;
|
||||
}
|
||||
}
|
||||
45
src/renderer/components/layout/tab-layout.tsx
Normal file
45
src/renderer/components/layout/tab-layout.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user