Extension support page (#1112)

Signed-off-by: Roman <ixrock@gmail.com>
Co-authored-by: Jim Ehrismann <40840436+jim-docker@users.noreply.github.com>
This commit is contained in:
RomanandJim Ehrismann authored and GitHub committed 2020-10-24 09:24:54 +03:00
1 parent ce995f3deb
commit f3a0059355
67 files changed
+4511 -553

No files matched your search

@@ -1,33 +0,0 @@
import { observable } from "mobx"
import React from "react"
export interface KubeObjectMenuComponents {
MenuItem: React.ComponentType<any>;
}
export interface KubeObjectMenuRegistration {
kind: string;
apiVersions: string[];
components: KubeObjectMenuComponents;
}
export class KubeObjectMenuRegistry {
items = observable.array<KubeObjectMenuRegistration>([], { deep: false });
add(item: KubeObjectMenuRegistration) {
this.items.push(item)
return () => {
this.items.replace(
this.items.filter(c => c !== item)
)
};
}
getItemsForKind(kind: string, apiVersion: string) {
return this.items.filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion)
})
}
}
export const kubeObjectMenuRegistry = new KubeObjectMenuRegistry()
+1
View File
@@ -1,4 +1,5 @@
import "./components/app.scss"
import React from "react";
import * as Mobx from "mobx"
import * as MobxReact from "mobx-react"
@@ -1,87 +1,51 @@
.ClusterSettings {
.WizardLayout {
grid-template-columns: unset;
grid-template-rows: 76px 1fr;
padding: 0;
$spacing: $padding * 3;
.head-col {
justify-content: space-between;
> .content-wrapper {
--flex-gap: #{$spacing};
}
:nth-child(2) {
flex: 1 0 0;
}
}
// TODO: move sub-component styles to separate files
.admin-note {
font-size: small;
opacity: 0.5;
margin-left: $margin;
}
.content-col {
margin: 0;
padding-top: $padding * 3;
background-color: $clusterSettingsBackground;
.button-area {
margin-top: $margin * 2;
}
.SubTitle {
text-transform: none;
}
.file-loader {
margin-top: $margin * 2;
}
> div {
margin-top: $margin * 5;
}
.status-table {
margin: $spacing 0;
.admin-note {
font-size: small;
opacity: 0.5;
margin-left: $margin;
}
.Table {
border: 1px solid var(--drawerSubtitleBackground);
border-radius: $radius;
.button-area {
margin-top: $margin * 2;
}
.TableRow {
&:not(:last-of-type) {
border-bottom: 1px solid var(--drawerSubtitleBackground);
}
.file-loader {
margin-top: $margin * 2;
}
.value {
flex-grow: 2;
word-break: break-word;
color: var(--textColorSecondary);
}
.hint {
font-size: smaller;
opacity: 0.8;
}
p + p, .hint + p {
padding-top: $padding;
}
}
.status-table {
margin: $margin * 3 0;
.Table {
border: 1px solid var(--drawerSubtitleBackground);
border-radius: $radius;
.TableRow {
&:not(:last-of-type) {
border-bottom: 1px solid var(--drawerSubtitleBackground);
}
.value {
flex-grow: 2;
word-break: break-word;
color: var(--textColorSecondary);
}
.link {
@include pseudo-link;
}
.link {
@include pseudo-link;
}
}
}
}
.Input, .Select {
margin-top: 10px;
}
.Select {
&__control {
box-shadow: 0 0 0 1px $borderFaintColor;
}
}
.Input, .Select {
margin-top: $padding;
}
}
@@ -1,21 +1,19 @@
import "./cluster-settings.scss";
import React from "react";
import { observer, disposeOnUnmount } from "mobx-react";
import { autorun } from "mobx";
import { disposeOnUnmount, observer } 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 { 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";
import { PageLayout } from "../layout/page-layout";
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
}
@@ -27,7 +25,6 @@ export class ClusterSettings extends React.Component<Props> {
}
async componentDidMount() {
window.addEventListener('keydown', this.onEscapeKey);
disposeOnUnmount(this,
autorun(() => {
this.refreshCluster();
@@ -35,51 +32,29 @@ export class ClusterSettings extends React.Component<Props> {
)
}
componentWillUnmount() {
window.removeEventListener('keydown', this.onEscapeKey);
}
onEscapeKey = (evt: KeyboardEvent) => {
if (evt.code === "Escape") {
evt.stopPropagation();
this.close();
}
}
refreshCluster = async () => {
if(this.cluster) {
if (this.cluster) {
await clusterIpc.activate.invokeFromRenderer(this.cluster.id);
await clusterIpc.refresh.invokeFromRenderer(this.cluster.id);
}
}
close() {
navigate("/");
}
render() {
const cluster = this.cluster
if (!cluster) return null;
const header = (
<>
<ClusterIcon
cluster={cluster}
showErrors={false}
showTooltip={false}
/>
<ClusterIcon cluster={cluster} showErrors={false} showTooltip={false}/>
<h2>{cluster.preferences.clusterName}</h2>
<Icon material="close" onClick={this.close} big/>
</>
);
return (
<div className="ClusterSettings">
<WizardLayout header={header} centered>
<Status cluster={cluster}></Status>
<General cluster={cluster}></General>
<Features cluster={cluster}></Features>
<Removal cluster={cluster}></Removal>
</WizardLayout>
</div>
<PageLayout className="ClusterSettings" header={header}>
<Status cluster={cluster}></Status>
<General cluster={cluster}></General>
<Features cluster={cluster}></Features>
<Removal cluster={cluster}></Removal>
</PageLayout>
);
}
}
@@ -41,10 +41,10 @@ export class ClusterHomeDirSetting extends React.Component<Props> {
onBlur={this.save}
placeholder="$HOME"
/>
<span className="hint">
<small className="hint">
An explicit start path where the terminal will be launched,{" "}
this is used as the current working directory (cwd) for the shell process.
</span>
</small>
</>
);
}
@@ -90,7 +90,7 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
}}
options={options}
/>
<span className="hint">What query format is used to fetch metrics from Prometheus</span>
<small className="hint">What query format is used to fetch metrics from Prometheus</small>
{this.canEditPrometheusPath && (
<>
<p>Prometheus service address.</p>
@@ -101,10 +101,10 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
onBlur={this.onSavePath}
placeholder="<namespace>/<service>:<port>"
/>
<span className="hint">
<small className="hint">
An address to an existing Prometheus installation{" "}
({'<namespace>/<service>:<port>'}). Lens tries to auto-detect address if left empty.
</span>
</small>
</>
)}
</>
+3 -3
View File
@@ -55,7 +55,7 @@ export class Nodes extends React.Component<Props> {
max={cores}
value={usage}
tooltip={{
position: TooltipPosition.BOTTOM,
preferredPositions: TooltipPosition.BOTTOM,
children: _i18n._(t`CPU:`) + ` ${Math.ceil(usage * 100) / cores}\%, ` + _i18n._(t`cores:`) + ` ${cores}`
}}
/>
@@ -72,7 +72,7 @@ export class Nodes extends React.Component<Props> {
max={capacity}
value={usage}
tooltip={{
position: TooltipPosition.BOTTOM,
preferredPositions: TooltipPosition.BOTTOM,
children: _i18n._(t`Memory:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}`
}}
/>
@@ -89,7 +89,7 @@ export class Nodes extends React.Component<Props> {
max={capacity}
value={usage}
tooltip={{
position: TooltipPosition.BOTTOM,
preferredPositions: TooltipPosition.BOTTOM,
children: _i18n._(t`Disk:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}`
}}
/>
@@ -1,60 +1,24 @@
.Preferences {
position: fixed!important; // Allows to cover ClustersMenu
z-index: 1;
$spacing: $padding * 2;
.WizardLayout {
grid-template-columns: unset;
grid-template-rows: 76px 1fr;
padding: 0;
.repos {
position: relative;
.content-col {
padding: $padding * 8 0;
background-color: $clusterSettingsBackground;
h2 {
margin-bottom: $margin * 2;
&:not(:first-child) {
margin-top: $margin * 3;
}
}
.SubTitle {
text-transform: none;
margin: 0!important;
}
.repos {
position: relative;
.Badge {
display: flex;
margin: 0;
margin-bottom: 1px;
padding: $padding $padding * 2;
}
}
.hint {
margin-top: -$margin;
}
.Badge {
display: flex;
margin: 0;
margin-bottom: 1px;
padding: $padding $spacing;
}
}
.is-mac & {
.WizardLayout .head-col {
padding-top: 32px;
overflow: hidden;
.Icon {
margin-top: -$margin * 2;
}
.extensions {
h2 {
margin: $spacing 0;
}
}
.Select {
&__control {
box-shadow: 0 0 0 1px $borderFaintColor;
&:empty {
display: none;
}
}
}
@@ -1,10 +1,10 @@
import "./preferences.scss"
import React from "react";
import { observer } from "mobx-react";
import { action, computed, observable } from "mobx";
import { t, Trans } from "@lingui/macro";
import { _i18n } from "../../i18n";
import { WizardLayout } from "../layout/wizard-layout";
import { Icon } from "../icon";
import { Select, SelectOption } from "../select";
import { userStore } from "../../../common/user-store";
@@ -14,10 +14,10 @@ import { Checkbox } from "../checkbox";
import { Notifications } from "../notifications";
import { Badge } from "../badge";
import { themeStore } from "../../theme.store";
import { history } from "../../navigation";
import { Tooltip } from "../tooltip";
import { KubectlBinaries } from "./kubectl-binaries";
import { appPreferenceRegistry } from "../../../extensions/app-preference-registry";
import { appPreferenceRegistry } from "../../../extensions/registries/app-preference-registry";
import { PageLayout } from "../layout/page-layout";
@observer
export class Preferences extends React.Component {
@@ -41,21 +41,9 @@ export class Preferences extends React.Component {
}
async componentDidMount() {
window.addEventListener('keydown', this.onEscapeKey);
await this.loadHelmRepos();
}
componentWillUnmount() {
window.removeEventListener('keydown', this.onEscapeKey);
}
onEscapeKey = (evt: KeyboardEvent) => {
if (evt.code === "Escape") {
evt.stopPropagation();
history.goBack();
}
}
@action
async loadHelmRepos() {
this.helmLoading = true;
@@ -115,91 +103,85 @@ export class Preferences extends React.Component {
render() {
const { preferences } = userStore;
const extensionPreferences = appPreferenceRegistry.preferences
const header = (
<>
<h2>Preferences</h2>
<Icon material="close" big onClick={history.goBack}/>
</>
);
const header = <h2><Trans>Preferences</Trans></h2>;
return (
<div className="Preferences">
<WizardLayout header={header} centered>
<h2><Trans>Color Theme</Trans></h2>
<Select
options={this.themeOptions}
value={preferences.colorTheme}
onChange={({ value }: SelectOption) => preferences.colorTheme = value}
/>
<PageLayout showOnTop className="Preferences" header={header}>
<h2><Trans>Color Theme</Trans></h2>
<Select
options={this.themeOptions}
value={preferences.colorTheme}
onChange={({ value }: SelectOption) => preferences.colorTheme = value}
/>
<h2><Trans>HTTP Proxy</Trans></h2>
<Input
theme="round-black"
placeholder={_i18n._(t`Type HTTP proxy url (example: http://proxy.acme.org:8080)`)}
value={this.httpProxy}
onChange={v => this.httpProxy = v}
onBlur={() => preferences.httpsProxy = this.httpProxy}
/>
<small className="hint">
<Trans>Proxy is used only for non-cluster communication.</Trans>
</small>
<h2><Trans>HTTP Proxy</Trans></h2>
<Input
theme="round-black"
placeholder={_i18n._(t`Type HTTP proxy url (example: http://proxy.acme.org:8080)`)}
value={this.httpProxy}
onChange={v => this.httpProxy = v}
onBlur={() => preferences.httpsProxy = this.httpProxy}
/>
<small className="hint">
<Trans>Proxy is used only for non-cluster communication.</Trans>
</small>
<KubectlBinaries preferences={preferences} />
<KubectlBinaries preferences={preferences}/>
<h2><Trans>Helm</Trans></h2>
<Select
placeholder={<Trans>Repositories</Trans>}
isLoading={this.helmLoading}
isDisabled={this.helmLoading}
options={this.helmOptions}
onChange={this.onRepoSelect}
formatOptionLabel={this.formatHelmOptionLabel}
controlShouldRenderValue={false}
/>
<div className="repos flex gaps column">
{Array.from(this.helmAddedRepos).map(([name, repo]) => {
const tooltipId = `message-${name}`;
return (
<Badge key={name} className="added-repo flex gaps align-center justify-space-between">
<span id={tooltipId} className="repo">{name}</span>
<Icon
material="delete"
onClick={() => this.removeRepo(repo)}
tooltip={<Trans>Remove</Trans>}
/>
<Tooltip targetId={tooltipId} formatters={{ narrow: true }}>
{repo.url}
</Tooltip>
</Badge>
)
})}
</div>
<h2><Trans>Certificate Trust</Trans></h2>
<Checkbox
label={<Trans>Allow untrusted Certificate Authorities</Trans>}
value={preferences.allowUntrustedCAs}
onChange={v => preferences.allowUntrustedCAs = v}
/>
<small className="hint">
<Trans>This will make Lens to trust ANY certificate authority without any validations.</Trans>{" "}
<Trans>Needed with some corporate proxies that do certificate re-writing.</Trans>{" "}
<Trans>Does not affect cluster communications!</Trans>
</small>
{extensionPreferences.map(({title, components: { Hint, Input}}) => {
<h2><Trans>Helm</Trans></h2>
<Select
placeholder={<Trans>Repositories</Trans>}
isLoading={this.helmLoading}
isDisabled={this.helmLoading}
options={this.helmOptions}
onChange={this.onRepoSelect}
formatOptionLabel={this.formatHelmOptionLabel}
controlShouldRenderValue={false}
/>
<div className="repos flex gaps column">
{Array.from(this.helmAddedRepos).map(([name, repo]) => {
const tooltipId = `message-${name}`;
return (
<div key={title}>
<Badge key={name} className="added-repo flex gaps align-center justify-space-between">
<span id={tooltipId} className="repo">{name}</span>
<Icon
material="delete"
onClick={() => this.removeRepo(repo)}
tooltip={<Trans>Remove</Trans>}
/>
<Tooltip targetId={tooltipId} formatters={{ narrow: true }}>
{repo.url}
</Tooltip>
</Badge>
)
})}
</div>
<h2><Trans>Certificate Trust</Trans></h2>
<Checkbox
label={<Trans>Allow untrusted Certificate Authorities</Trans>}
value={preferences.allowUntrustedCAs}
onChange={v => preferences.allowUntrustedCAs = v}
/>
<small className="hint">
<Trans>This will make Lens to trust ANY certificate authority without any validations.</Trans>{" "}
<Trans>Needed with some corporate proxies that do certificate re-writing.</Trans>{" "}
<Trans>Does not affect cluster communications!</Trans>
</small>
<div className="extensions flex column gaps">
{appPreferenceRegistry.getItems().map(({ title, components: { Hint, Input } }, index) => {
return (
<div key={index} className="preference">
<h2>{title}</h2>
<Input />
<Input/>
<small className="hint">
<Hint />
<Hint/>
</small>
</div>
)
})}
</WizardLayout>
</div>
</div>
</PageLayout>
);
}
}
@@ -3,9 +3,9 @@ import "./service-accounts.scss";
import React from "react";
import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { ServiceAccount, serviceAccountsApi } from "../../api/endpoints/service-accounts.api";
import { ServiceAccount } from "../../api/endpoints/service-accounts.api";
import { RouteComponentProps } from "react-router";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { MenuItem } from "../menu";
import { openServiceAccountKubeConfig } from "../kubeconfig-dialog";
import { Icon } from "../icon";
@@ -13,7 +13,7 @@ import { KubeObjectListLayout } from "../kube-object";
import { IServiceAccountsRouteParams } from "../+user-management";
import { serviceAccountsStore } from "./service-accounts.store";
import { CreateServiceAccountDialog } from "./create-service-account-dialog";
import { kubeObjectMenuRegistry } from "../../api/kube-object-menu-registry";
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
enum sortBy {
name = "name",
@@ -4,19 +4,19 @@ import React from "react";
import { observer } from "mobx-react";
import { RouteComponentProps } from "react-router";
import { t, Trans } from "@lingui/macro";
import { CronJob, cronJobApi } from "../../api/endpoints/cron-job.api";
import { CronJob } from "../../api/endpoints/cron-job.api";
import { MenuItem } from "../menu";
import { Icon } from "../icon";
import { cronJobStore } from "./cronjob.store";
import { jobStore } from "../+workloads-jobs/job.store";
import { eventStore } from "../+events/event.store";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { ICronJobsRouteParams } from "../+workloads";
import { KubeObjectListLayout } from "../kube-object";
import { KubeEventIcon } from "../+events/kube-event-icon";
import { _i18n } from "../../i18n";
import { CronJobTriggerDialog } from "./cronjob-trigger-dialog";
import { kubeObjectMenuRegistry } from "../../api/kube-object-menu-registry";
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
enum sortBy {
name = "name",
@@ -4,8 +4,8 @@ import React from "react";
import { observer } from "mobx-react";
import { RouteComponentProps } from "react-router";
import { t, Trans } from "@lingui/macro";
import { Deployment, deploymentApi } from "../../api/endpoints";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { Deployment } from "../../api/endpoints";
import { KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { MenuItem } from "../menu";
import { Icon } from "../icon";
import { DeploymentScaleDialog } from "./deployment-scale-dialog";
@@ -21,8 +21,7 @@ import { cssNames } from "../../utils";
import kebabCase from "lodash/kebabCase";
import orderBy from "lodash/orderBy";
import { KubeEventIcon } from "../+events/kube-event-icon";
import { kubeObjectMenuRegistry } from "../../api/kube-object-menu-registry";
import { DeploymentDetails } from "./deployment-details";
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
enum sortBy {
name = "name",
+4 -5
View File
@@ -35,11 +35,10 @@ import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store
import logger from "../../main/logger";
import { clusterIpc } from "../../common/cluster-ipc";
import { webFrame } from "electron";
import { pageRegistry } from "../../extensions/page-registry";
import { pageRegistry } from "../../extensions/registries/page-registry";
import { DynamicPage } from "../../extensions/dynamic-page";
import { extensionLoader } from "../../extensions/extension-loader";
import { getLensRuntime } from "../../extensions/lens-runtime";
import { appEventBus } from "../../common/event-bus"
import { appEventBus } from "../../common/event-bus"
@observer
export class App extends React.Component {
@@ -51,7 +50,7 @@ export class App extends React.Component {
await clusterIpc.setFrameId.invokeFromRenderer(clusterId, frameId);
await getHostedCluster().whenReady; // cluster.activate() is done at this point
extensionLoader.loadOnClusterRenderer(getLensRuntime)
extensionLoader.loadOnClusterRenderer();
appEventBus.emit({name: "cluster", action: "open", params: {
clusterId: clusterId
}})
@@ -83,7 +82,7 @@ export class App extends React.Component {
<Route component={UserManagement} {...usersManagementRoute}/>
<Route component={Apps} {...appsRoute}/>
{pageRegistry.clusterPages.map(page => {
return <Route {...page} key={page.path} render={() => <DynamicPage page={page}/>}/>
return <Route {...page} key={String(page.path)} render={() => <DynamicPage page={page}/>}/>
})}
<Redirect exact from="/" to={this.startURL}/>
<Route component={NotFound}/>
@@ -1,9 +1,11 @@
import "./bottom-bar.scss"
import React from "react";
import { observer } from "mobx-react";
import { Icon } from "../icon";
import { WorkspaceMenu } from "../+workspaces/workspace-menu";
import { workspaceStore } from "../../../common/workspace-store";
import { statusBarRegistry } from "../../../extensions/registries";
@observer
export class BottomBar extends React.Component {
@@ -11,11 +13,19 @@ export class BottomBar extends React.Component {
const { currentWorkspace } = workspaceStore;
return (
<div className="BottomBar flex gaps">
<div id="current-workspace" className="flex gaps align-center box">
<div id="current-workspace" className="flex gaps align-center">
<Icon small material="layers"/>
<span className="workspace-name">{currentWorkspace.name}</span>
</div>
<WorkspaceMenu htmlFor="current-workspace"/>
<WorkspaceMenu
htmlFor="current-workspace"
/>
<div className="extensions box grow flex gaps justify-flex-end">
{statusBarRegistry.getItems().map(({ icon }, index) => {
if (!icon) return;
return <React.Fragment key={index}>{icon}</React.Fragment>
})}
</div>
</div>
)
}
@@ -14,7 +14,7 @@ import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route";
import { clusterStore } from "../../../common/cluster-store";
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
import { pageRegistry } from "../../../extensions/page-registry";
import { pageRegistry } from "../../../extensions/registries/page-registry";
@observer
export class ClusterManager extends React.Component {
@@ -63,8 +63,8 @@ export class ClusterManager extends React.Component {
<Route component={AddCluster} {...addClusterRoute} />
<Route component={ClusterView} {...clusterViewRoute} />
<Route component={ClusterSettings} {...clusterSettingsRoute} />
{pageRegistry.globalPages.map(({ path, components: { Page } }) => {
return <Route key={path} path={path} component={Page}/>
{pageRegistry.globalPages.map(({ path, url = String(path), components: { Page } }) => {
return <Route key={url} path={path} component={Page}/>
})}
<Redirect exact to={this.startUrl} />
</Switch>
@@ -67,7 +67,7 @@
}
}
> .dynamic-pages {
> .extensions {
&:not(:empty) {
padding-top: $spacing;
}
@@ -11,7 +11,7 @@ import { ClusterId, clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store";
import { ClusterIcon } from "../cluster-icon";
import { Icon } from "../icon";
import { cssNames, IClassName, autobind } from "../../utils";
import { autobind, cssNames, IClassName } from "../../utils";
import { Badge } from "../badge";
import { navigate } from "../../navigation";
import { addClusterURL } from "../+add-cluster";
@@ -21,8 +21,8 @@ import { Tooltip } from "../tooltip";
import { ConfirmDialog } from "../confirm-dialog";
import { clusterIpc } from "../../../common/cluster-ipc";
import { clusterViewURL } from "./cluster-view.route";
import { DragDropContext, Droppable, Draggable, DropResult, DroppableProvided, DraggableProvided } from "react-beautiful-dnd";
import { pageRegistry } from "../../../extensions/page-registry";
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
import { pageRegistry } from "../../../extensions/registries/page-registry";
interface Props {
className?: IClassName;
@@ -155,9 +155,10 @@ export class ClustersMenu extends React.Component<Props> {
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>} />
)}
</div>
<div className="dynamic-pages">
{pageRegistry.globalPages.map(({ path, components: { MenuIcon } }) => {
return <MenuIcon key={path} onClick={() => navigate(path)}/>
<div className="extensions">
{pageRegistry.globalPages.map(({ path, url = String(path), components: { MenuIcon } }) => {
if (!MenuIcon) return;
return <MenuIcon key={url} onClick={() => navigate(url)}/>
})}
</div>
</div>
+9 -1
View File
@@ -1,7 +1,15 @@
// Custom fonts
@import "~material-design-icons/iconfont/material-icons.css";
@import "~typeface-roboto/index.css";
// Material Design Icons, used primarily in icon.tsx
// Latest: https://github.com/google/material-design-icons/tree/master/font
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: url("../fonts/MaterialIcons-Regular.ttf") format("truetype");
}
// Patched RobotoMono font with icons
// RobotoMono Windows Compatible for using in terminal
// https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/RobotoMono
@@ -6,7 +6,7 @@ import { editResourceTab } from "../dock/edit-resource.store";
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
import { hideDetails } from "../../navigation";
import { apiManager } from "../../api/api-manager";
import { kubeObjectMenuRegistry } from "../../api/kube-object-menu-registry";
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
export interface KubeObjectMenuProps<T extends KubeObject = any> extends MenuActionsProps {
object: T;
@@ -0,0 +1,64 @@
.PageLayout {
$spacing: $padding * 2;
position: relative;
height: 100%;
display: grid !important;
grid-template-rows: min-content 1fr;
// covers whole app view area
&.top {
position: fixed !important; // allow to cover ClustersMenu
z-index: 1;
// adds extra space for traffic-light top buttons (mac only)
.is-mac & > .header {
padding-top: $spacing * 2;
}
}
> .header {
position: sticky;
padding: $spacing;
background-color: $layoutTabsBackground;
}
> .content-wrapper {
@include custom-scrollbar-themed;
padding: $spacing * 2;
> .content {
margin: 0 auto;
width: 60%;
min-width: 570px;
max-width: 1000px;
}
}
h2:not(:first-of-type) {
margin-top: $spacing;
}
p {
line-height: 140%;
}
a {
color: $colorInfo;
}
.SubTitle {
text-transform: none;
margin-bottom: 0 !important;
+ * + .hint {
margin-top: -$padding / 2;
}
}
.Select {
&__control {
box-shadow: 0 0 0 1px $borderFaintColor;
}
}
}
@@ -0,0 +1,82 @@
import "./page-layout.scss"
import React from "react";
import { observer } from "mobx-react";
import { autobind, cssNames, IClassName } from "../../utils";
import { Icon } from "../icon";
import { navigation } from "../../navigation";
export interface PageLayoutProps extends React.DOMAttributes<any> {
className?: IClassName;
header: React.ReactNode;
headerClass?: IClassName;
contentClass?: IClassName;
provideBackButtonNavigation?: boolean;
contentGaps?: boolean;
showOnTop?: boolean; // covers whole app view
back?: (evt: React.MouseEvent | KeyboardEvent) => void;
}
const defaultProps: Partial<PageLayoutProps> = {
provideBackButtonNavigation: true,
contentGaps: true,
}
@observer
export class PageLayout extends React.Component<PageLayoutProps> {
static defaultProps = defaultProps as object;
@autobind()
back(evt?: React.MouseEvent | KeyboardEvent) {
if (this.props.back) {
this.props.back(evt);
} else {
navigation.goBack();
}
}
async componentDidMount() {
window.addEventListener('keydown', this.onEscapeKey);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.onEscapeKey);
}
onEscapeKey = (evt: KeyboardEvent) => {
if (!this.props.provideBackButtonNavigation) {
return;
}
if (evt.code === "Escape") {
evt.stopPropagation();
this.back(evt);
}
}
render() {
const {
contentClass, header, headerClass, provideBackButtonNavigation,
contentGaps, showOnTop, children, ...elemProps
} = this.props;
const className = cssNames("PageLayout", { top: showOnTop }, this.props.className);
return (
<div {...elemProps} className={className}>
<div className={cssNames("header flex gaps align-center", headerClass)}>
{header}
{provideBackButtonNavigation && (
<Icon
big material="close"
className="back box right"
onClick={this.back}
/>
)}
</div>
<div className="content-wrapper">
<div className={cssNames("content", contentGaps && "flex column gaps", contentClass)}>
{children}
</div>
</div>
</div>
)
}
}
+18 -18
View File
@@ -28,7 +28,7 @@ import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resourc
import { CustomResources } from "../+custom-resources/custom-resources";
import { navigation } from "../../navigation";
import { isAllowedResource } from "../../../common/rbac"
import { pageRegistry } from "../../../extensions/page-registry";
import { pageRegistry } from "../../../extensions/registries/page-registry";
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
type SidebarContextValue = {
@@ -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
@@ -97,14 +97,14 @@ export class Sidebar extends React.Component<Props> {
isHidden={!isAllowedResource("nodes")}
url={clusterURL()}
text={<Trans>Cluster</Trans>}
icon={<Icon svg="kube" />}
icon={<Icon svg="kube"/>}
/>
<SidebarNavItem
id="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,14 +139,14 @@ 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")}
url={namespacesURL()}
icon={<Icon material="layers" />}
icon={<Icon material="layers"/>}
text={<Trans>Namespaces</Trans>}
/>
<SidebarNavItem
@@ -154,7 +154,7 @@ export class Sidebar extends React.Component<Props> {
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,7 +170,7 @@ 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
@@ -179,17 +179,17 @@ export class Sidebar extends React.Component<Props> {
url={crdURL()}
subMenus={CustomResources.tabRoutes}
routePath={crdRoute.path}
icon={<Icon material="extension" />}
icon={<Icon material="extension"/>}
text={<Trans>Custom Resources</Trans>}
>
{this.renderCustomResources()}
</SidebarNavItem>
{pageRegistry.clusterPages.map(({ path, title, components: { MenuIcon } }) => {
{pageRegistry.clusterPages.map(({ path, title, url = String(path), components: { MenuIcon } }) => {
if (!MenuIcon) return;
return (
<SidebarNavItem
key={path}
id={`extension-${path}`}
url={path}
key={url} id={`sidebar_item_${url}`}
url={url}
routePath={path}
text={title}
icon={<MenuIcon/>}
@@ -255,7 +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 }) => (
@@ -8,13 +8,9 @@
grid-template-columns: 1fr 40%;
> * {
@include custom-scrollbar;
@include custom-scrollbar-themed;
--flex-gap: #{$spacing};
padding: $spacing;
.theme-light & {
@include custom-scrollbar(dark);
}
}
> .head-col {
+16
View File
@@ -6,6 +6,22 @@
@import "table/table.mixins";
@import "+network/network-mixins";
// todo: re-use in other places with theming
@mixin custom-scrollbar-themed($invert: false) {
@if ($invert) {
@include custom-scrollbar(dark);
.theme-light & {
@include custom-scrollbar(light);
}
} @else {
// fits better with dark background
@include custom-scrollbar(light);
.theme-light & {
@include custom-scrollbar(dark);
}
}
}
@mixin custom-scrollbar($theme: light, $size: 7px, $borderSpacing: 5px) {
$themes: (
light: #5f6064,
+55 -24
View File
@@ -11,6 +11,10 @@ export enum TooltipPosition {
BOTTOM = "bottom",
LEFT = "left",
RIGHT = "right",
TOP_LEFT = "top_left",
TOP_RIGHT = "top_right",
BOTTOM_LEFT = "bottom_left",
BOTTOM_RIGHT = "bottom_right",
}
export interface TooltipProps {
@@ -19,7 +23,7 @@ export interface TooltipProps {
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
position?: TooltipPosition;
preferredPositions?: TooltipPosition | TooltipPosition[];
className?: IClassName;
formatters?: TooltipContentFormatters;
style?: React.CSSProperties;
@@ -82,17 +86,25 @@ export class Tooltip extends React.Component<TooltipProps> {
@autobind()
refreshPosition() {
const { position } = this.props;
const { preferredPositions } = this.props;
const { elem, targetElem } = this;
const positionPreference = new Set<TooltipPosition>();
if (typeof position !== "undefined") {
positionPreference.add(position);
let positions = new Set<TooltipPosition>([
TooltipPosition.RIGHT,
TooltipPosition.BOTTOM,
TooltipPosition.TOP,
TooltipPosition.LEFT,
TooltipPosition.TOP_RIGHT,
TooltipPosition.TOP_LEFT,
TooltipPosition.BOTTOM_RIGHT,
TooltipPosition.BOTTOM_LEFT,
]);
if (preferredPositions) {
positions = new Set([
...[preferredPositions].flat(),
...positions,
])
}
positionPreference.add(TooltipPosition.RIGHT)
.add(TooltipPosition.BOTTOM)
.add(TooltipPosition.TOP)
.add(TooltipPosition.LEFT)
// reset position first and get all possible client-rect area for tooltip element
this.setPosition({ left: 0, top: 0 });
@@ -102,20 +114,20 @@ export class Tooltip extends React.Component<TooltipProps> {
const { innerWidth: viewportWidth, innerHeight: viewportHeight } = window;
// find proper position
for (const pos of positionPreference) {
for (const pos of positions) {
const { left, top, right, bottom } = this.getPosition(pos, selfBounds, targetBounds)
const fitsToWindow = left >= 0 && top >= 0 && right <= viewportWidth && bottom <= viewportHeight;
if (fitsToWindow) {
this.activePosition = pos;
this.setPosition({ top, left });
return;
}
}
const preferedPosition = Array.from(positionPreference)[0];
const { left, top } = this.getPosition(preferedPosition, selfBounds, targetBounds)
this.activePosition = preferedPosition;
// apply fallback position if nothing helped from above
const fallbackPosition = Array.from(positions)[0];
const { left, top } = this.getPosition(fallbackPosition, selfBounds, targetBounds)
this.activePosition = fallbackPosition;
this.setPosition({ left, top });
}
@@ -125,35 +137,54 @@ export class Tooltip extends React.Component<TooltipProps> {
elemStyle.top = pos.top + "px"
}
protected getPosition(position: TooltipPosition, selfBounds: DOMRect, targetBounds: DOMRect) {
let left: number
let top: number
protected getPosition(position: TooltipPosition, tooltipBounds: DOMRect, targetBounds: DOMRect) {
let left: number;
let top: number;
const offset = this.props.offset;
const horizontalCenter = targetBounds.left + (targetBounds.width - selfBounds.width) / 2;
const verticalCenter = targetBounds.top + (targetBounds.height - selfBounds.height) / 2;
const horizontalCenter = targetBounds.left + (targetBounds.width - tooltipBounds.width) / 2;
const verticalCenter = targetBounds.top + (targetBounds.height - tooltipBounds.height) / 2;
const topCenter = targetBounds.top - tooltipBounds.height - offset;
const bottomCenter = targetBounds.bottom + offset;
switch (position) {
case "top":
left = horizontalCenter;
top = targetBounds.top - selfBounds.height - offset;
top = topCenter;
break;
case "bottom":
left = horizontalCenter;
top = targetBounds.bottom + offset;
top = bottomCenter;
break;
case "left":
top = verticalCenter;
left = targetBounds.left - selfBounds.width - offset;
left = targetBounds.left - tooltipBounds.width - offset;
break;
case "right":
top = verticalCenter;
left = targetBounds.right + offset;
break;
case "top_left":
left = targetBounds.left;
top = topCenter;
break;
case "top_right":
default:
left = targetBounds.right - tooltipBounds.width;
top = topCenter;
break;
case "bottom_left":
top = bottomCenter;
left = targetBounds.left;
break;
case "bottom_right":
top = bottomCenter;
left = targetBounds.right - tooltipBounds.width;
break;
}
return {
left: left,
top: top,
right: left + selfBounds.width,
bottom: top + selfBounds.height,
right: left + tooltipBounds.width,
bottom: top + tooltipBounds.height,
};
}
Binary file not shown.
+1 -1
View File
@@ -14,7 +14,7 @@ export interface ILanguage {
export const _i18n = setupI18n({
missing: (message, id) => {
console.warn('Missing localization:', message, id);
// console.warn('Missing localization:', message, id);
return id;
}
});
+1 -2
View File
@@ -12,12 +12,11 @@ import { WhatsNew, whatsNewRoute } from "./components/+whats-new";
import { Notifications } from "./components/notifications";
import { ConfirmDialog } from "./components/confirm-dialog";
import { extensionLoader } from "../extensions/extension-loader";
import { getLensRuntime } from "../extensions/lens-runtime";
@observer
export class LensApp extends React.Component {
static async init() {
extensionLoader.loadOnMainRenderer(getLensRuntime)
extensionLoader.loadOnClusterManagerRenderer();
}
render() {