Merge branch 'master' into extensions-api

# Conflicts:
#	src/renderer/components/app.tsx
#	src/renderer/components/cluster-manager/clusters-menu.scss
#	src/renderer/components/cluster-manager/clusters-menu.tsx
#	src/renderer/components/layout/main-layout.tsx
#	src/renderer/components/layout/sidebar.tsx
This commit is contained in:
Roman committed 2020-09-23 11:49:16 +03:00
commit 91b4823ec6
54 files changed
+619 -333

No files matched your search

+5 -5
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>
);
}
}
}
@@ -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>
+3 -3
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>
)
}
}
@@ -1,21 +1,38 @@
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";
import { getMatchedCluster } from "../cluster-manager/cluster-view.route";
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 {
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() {
@@ -29,12 +46,18 @@ export class ClusterSettings extends React.Component {
}
}
refreshCluster = () => {
if(this.cluster) {
clusterIpc.refresh.invokeFromRenderer(this.cluster.id);
}
}
close() {
navigate("/");
}
render() {
const cluster = getMatchedCluster();
const cluster = this.cluster
if (!cluster) return null;
const header = (
<>
+3 -3
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>
)
}
}
+3 -3
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>
)
}
}
@@ -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>
);
}
}
+3 -3
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>
)
}
}
@@ -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>
)
}
}
@@ -67,7 +67,7 @@ export class IngressDetails extends React.Component<Props> {
}
renderIngressPoints(ingressPoints: ILoadBalancerIngress[]) {
if (ingressPoints.length === 0) return null
if (!ingressPoints || ingressPoints.length === 0) return null
return (
<div>
<Table className="ingress-points">
+3 -3
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>
)
}
}
+3 -3
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>
)
}
}
+3 -3
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>
)
}
}
@@ -15,6 +15,7 @@ import { getDetailsUrl } from "../../navigation";
import { KubeObjectDetailsProps } from "../kube-object";
import { apiManager } from "../../api/api-manager";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { Icon } from "../icon";
interface Props extends KubeObjectDetailsProps<ServiceAccount> {
}
@@ -22,21 +23,25 @@ interface Props extends KubeObjectDetailsProps<ServiceAccount> {
@observer
export class ServiceAccountsDetails extends React.Component<Props> {
@observable secrets: Secret[];
@observable imagePullSecrets: Secret[];
@disposeOnUnmount
loadSecrets = autorun(async () => {
this.secrets = null;
this.imagePullSecrets = null;
const { object: serviceAccount } = this.props;
if (!serviceAccount) {
return;
}
const namespace = serviceAccount.getNs();
const secrets = serviceAccount.getSecrets().map(({ name }) => {
const secret = secretsStore.getByName(name, namespace);
if (!secret) return secretsStore.load({ name, namespace });
return secret;
return secretsStore.load({ name, namespace });
});
this.secrets = await Promise.all(secrets);
const imagePullSecrets = serviceAccount.getImagePullSecrets().map(async({ name }) => {
return secretsStore.load({ name, namespace }).catch(_err => { return this.generateDummySecretObject(name) });
});
this.imagePullSecrets = await Promise.all(imagePullSecrets)
})
renderSecrets() {
@@ -49,15 +54,46 @@ export class ServiceAccountsDetails extends React.Component<Props> {
)
}
renderImagePullSecrets() {
const { imagePullSecrets } = this;
if (!imagePullSecrets) {
return <Spinner center/>
}
return this.renderSecretLinks(imagePullSecrets)
}
renderSecretLinks(secrets: Secret[]) {
return secrets.map(secret => {
return secrets.map((secret) => {
if (secret.getId() === null) {
return (
<div key={secret.getName()}>
{secret.getName()}
<Icon
small material="warning"
tooltip={<Trans>Secret is not found</Trans>}
/>
</div>
)
}
return (
<Link key={secret.getId()} to={getDetailsUrl(secret.selfLink)}>
{secret.getName()}
</Link>
)
}
)
})
}
generateDummySecretObject(name: string) {
return new Secret({
apiVersion: "v1",
kind: "Secret",
metadata: {
name: name,
uid: null,
selfLink: null,
resourceVersion: null
}
})
}
render() {
@@ -69,9 +105,7 @@ export class ServiceAccountsDetails extends React.Component<Props> {
secret.getNs() == serviceAccount.getNs() &&
secret.getAnnotations().some(annot => annot == `kubernetes.io/service-account.name: ${serviceAccount.getName()}`)
)
const imagePullSecrets = serviceAccount.getImagePullSecrets().map(({ name }) =>
secretsStore.getByName(name, serviceAccount.getNs())
)
const imagePullSecrets = serviceAccount.getImagePullSecrets()
return (
<div className="ServiceAccountsDetails">
<KubeObjectMeta object={serviceAccount}/>
@@ -83,7 +117,7 @@ export class ServiceAccountsDetails extends React.Component<Props> {
}
{imagePullSecrets.length > 0 &&
<DrawerItem name={<Trans>ImagePullSecrets</Trans>} className="links">
{this.renderSecretLinks(imagePullSecrets)}
{this.renderImagePullSecrets()}
</DrawerItem>
}
@@ -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>
)
}
}
+20 -18
View File
@@ -29,6 +29,7 @@ import { CronJobTriggerDialog } from "./+workloads-cronjobs/cronjob-trigger-dial
import { CustomResources } from "./+custom-resources/custom-resources";
import { crdRoute } from "./+custom-resources";
import { isAllowedResource } from "../../common/rbac";
import { MainLayout } from "./layout/main-layout";
import { ErrorBoundary } from "./error-boundary";
import { Terminal } from "./dock/terminal";
import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store";
@@ -60,24 +61,25 @@ 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}/>
{dynamicPages.clusterPages.map(({ path, components: { Page } }) => {
return <Route key={path} path={path} component={Page}/>
})}
<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}/>
{dynamicPages.clusterPages.map(({ path, components: { Page } }) => {
return <Route key={path} path={path} component={Page}/>
})}
<Redirect exact from="/" to={this.startURL}/>
<Route component={NotFound}/>
</Switch></MainLayout>
<Notifications/>
<ConfirmDialog/>
<KubeObjectDetails/>
+9 -2
View File
@@ -13,11 +13,18 @@ interface Props extends React.HTMLAttributes<any>, TooltipDecoratorProps {
export class Badge extends React.Component<Props> {
render() {
const { className, label, small, children, ...elemProps } = this.props;
return (
return <>
<span className={cssNames("Badge", { small }, className)} {...elemProps}>
{label}
{children}
</span>
);
{ /**
* This is a zero-width-space. It makes there be a word seperation
* between adjacent Badge's because <span>'s are ignored for browers'
* word detection algorithmns use for determining the extent of the
* text to highlight on multi-click sequences.
*/}
&#8203;
</>
}
}
+2 -3
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;
}
}
}
@@ -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;
}
@@ -3,7 +3,6 @@ import { ipcRenderer } from "electron";
import { matchPath, RouteProps } from "react-router";
import { buildURL, navigation } from "../../navigation";
import { clusterStore } from "../../../common/cluster-store";
import { clusterSettingsRoute } from "../+cluster-settings/cluster-settings.route";
export interface IClusterViewRouteParams {
clusterId: string;
@@ -19,10 +18,7 @@ export const clusterViewURL = buildURL<IClusterViewRouteParams>(clusterViewRoute
export function getMatchedClusterId(): string {
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
exact: true,
path: [
clusterViewRoute.path,
clusterSettingsRoute.path,
].flat(),
path: clusterViewRoute.path
})
if (matched) {
return matched.params.clusterId;
@@ -49,12 +49,11 @@
.Badge {
$boxSize: 17px;
$offset: -7px;
position: absolute;
bottom: 0px;
transform: translateX(-50%) translateY(50%);
font-size: $font-size-small;
right: $offset;
bottom: $offset;
line-height: $boxSize;
min-width: $boxSize;
min-height: $boxSize;
@@ -77,4 +76,4 @@
--size: 40px;
}
}
}
}
@@ -1,16 +1,17 @@
import type { Cluster } from "../../../main/cluster";
import "./clusters-menu.scss"
import { remote } from "electron"
import React from "react";
import { observer } from "mobx-react";
import { _i18n } from "../../i18n";
import { t, Trans } from "@lingui/macro";
import type { Cluster } from "../../../main/cluster";
import { userStore } from "../../../common/user-store";
import { ClusterId, clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store";
import { ClusterIcon } from "../cluster-icon";
import { Icon } from "../icon";
import { autobind, cssNames, IClassName } from "../../utils";
import { cssNames, IClassName, autobind } from "../../utils";
import { Badge } from "../badge";
import { navigate } from "../../navigation";
import { addClusterURL } from "../+add-cluster";
@@ -19,8 +20,8 @@ import { landingURL } from "../+landing-page";
import { Tooltip } from "../tooltip";
import { ConfirmDialog } from "../confirm-dialog";
import { clusterIpc } from "../../../common/cluster-ipc";
import { clusterViewURL, getMatchedClusterId } from "./cluster-view.route";
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
import { clusterViewURL } from "./cluster-view.route";
import { DragDropContext, Droppable, Draggable, DropResult, DroppableProvided, DraggableProvided } from "react-beautiful-dnd";
import { dynamicPages } from "../../../extensions/register-page";
interface Props {
@@ -36,6 +37,7 @@ export class ClustersMenu extends React.Component<Props> {
addCluster = () => {
navigate(addClusterURL());
clusterStore.setActive(null);
}
showContextMenu = (cluster: Cluster) => {
@@ -45,6 +47,7 @@ export class ClustersMenu extends React.Component<Props> {
menu.append(new MenuItem({
label: _i18n._(t`Settings`),
click: () => {
clusterStore.setActive(cluster.id);
navigate(clusterSettingsURL({
params: {
clusterId: cluster.id
@@ -58,6 +61,7 @@ export class ClustersMenu extends React.Component<Props> {
click: async () => {
if (clusterStore.isActive(cluster.id)) {
navigate(landingURL());
clusterStore.setActive(null);
}
await clusterIpc.disconnect.invokeFromRenderer(cluster.id);
}
@@ -113,26 +117,29 @@ export class ClustersMenu extends React.Component<Props> {
ref={provided.innerRef}
{...provided.droppableProps}
>
{clusters.map((cluster, index) => (
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
{(provided: DraggableProvided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<ClusterIcon
key={cluster.id}
showErrors={true}
cluster={cluster}
isActive={cluster.id === getMatchedClusterId()}
onClick={() => this.showCluster(cluster.id)}
onContextMenu={() => this.showContextMenu(cluster)}
/>
</div>
)}
</Draggable>
))}
{clusters.map((cluster, index) => {
const isActive = cluster.id === clusterStore.activeClusterId;
return (
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
{(provided: DraggableProvided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<ClusterIcon
key={cluster.id}
showErrors={true}
cluster={cluster}
isActive={isActive}
onClick={() => this.showCluster(cluster.id)}
onContextMenu={() => this.showContextMenu(cluster)}
/>
</div>
)}
</Draggable>
)}
)}
{provided.placeholder}
</div>
)}
@@ -143,9 +150,9 @@ export class ClustersMenu extends React.Component<Props> {
<Tooltip targetId="add-cluster-icon">
<Trans>Add Cluster</Trans>
</Tooltip>
<Icon big material="add" id="add-cluster-icon"/>
<Icon big material="add" id="add-cluster-icon" />
{newContexts.size > 0 && (
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>}/>
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>} />
)}
</div>
<div className="dynamic-pages">
@@ -48,6 +48,7 @@ export class EditorPanel extends React.Component<Props> {
onResize = () => {
this.editor.resize();
this.editor.focus();
}
onCursorPosChange = (pos: Ace.Point) => {
+9 -9
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>
);
}
}
}
+1
View File
@@ -152,6 +152,7 @@ export class Terminal {
onResize = () => {
if (!this.isActive) return;
this.fitLazy();
this.focus();
}
onActivate = () => {
@@ -44,7 +44,7 @@ export const isUrl: Validator = {
export const isPath: Validator = {
condition: ({ type }) => type === "text",
message: () => _i18n._(t`This field must be a path to an existing file`),
message: () => _i18n._(t`This field must be a valid path`),
validate: value => !value || fse.pathExistsSync(value),
}
@@ -73,4 +73,4 @@ export const accountId: Validator = {
export const conditionalValidators = [
isRequired, isEmail, isNumber, isUrl, minLength, maxLength
];
];
+12 -24
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)
}
}
}
+9 -39
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;
}
export interface MainLayoutProps {
className?: any;
tabs?: TabRoute[];
footer?: React.ReactNode;
headerClass?: string;
contentClass?: string;
footerClass?: string;
}
@@ -35,18 +25,17 @@ export class MainLayout extends React.Component<MainLayoutProps> {
@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<MainLayoutProps> {
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>
);
}
+37 -37
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";
@@ -44,21 +44,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}
@@ -68,8 +68,8 @@ export class Sidebar extends React.Component<Props> {
subMenus={submenus}
text={group}
/>
)
})
);
});
}
render() {
@@ -80,7 +80,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
@@ -94,17 +94,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"
@@ -113,7 +113,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"
@@ -122,7 +122,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"
@@ -131,7 +131,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"
@@ -139,22 +139,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
@@ -162,7 +162,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
@@ -170,16 +170,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()}
@@ -199,7 +199,7 @@ export class Sidebar extends React.Component<Props> {
</div>
</div>
</SidebarContext.Provider>
)
);
}
}
@@ -216,7 +216,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> {
@@ -229,15 +232,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;
@@ -252,10 +255,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 }) => (
@@ -265,18 +265,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
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;
}
}
@@ -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 @@
}
}
}
}
}
+13 -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()
@@ -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} />;
}
}