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

Kubeconfig syncs from Catalog

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2021-08-04 15:47:51 +03:00
parent 14f8343170
commit f589987e19
11 changed files with 217 additions and 66 deletions

View File

@ -20,12 +20,11 @@
*/
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { clusterActivateHandler, clusterDeleteHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store";
import { requestMain } from "../ipc";
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
import { addClusterURL } from "../routes";
import { app } from "electron";
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
import { HotbarStore } from "../hotbar-store";
@ -43,7 +42,6 @@ export interface KubernetesClusterPrometheusMetrics {
export interface KubernetesClusterSpec extends CatalogEntitySpec {
kubeconfigPath: string;
kubeconfigContext: string;
accessibleNamespaces?: string[];
metrics?: {
source: string;
prometheus?: KubernetesClusterPrometheusMetrics;
@ -149,7 +147,7 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
}
}
export class KubernetesClusterCategory extends CatalogCategory {
class KubernetesClusterCategory extends CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
public readonly kind = "CatalogCategory";
public metadata = {
@ -168,20 +166,8 @@ export class KubernetesClusterCategory extends CatalogCategory {
kind: "KubernetesCluster"
}
};
constructor() {
super();
this.on("catalogAddMenu", (ctx: CatalogEntityAddMenuContext) => {
ctx.menuItems.push({
icon: "text_snippet",
title: "Add from kubeconfig",
onClick: () => {
ctx.navigate(addClusterURL());
}
});
});
}
}
catalogCategoryRegistry.add(new KubernetesClusterCategory());
export const kubernetesClusterCategory = new KubernetesClusterCategory();
catalogCategoryRegistry.add(kubernetesClusterCategory);

View File

@ -123,6 +123,7 @@ export interface CatalogEntityContextMenu {
export interface CatalogEntityAddMenu extends CatalogEntityContextMenu {
icon: string;
defaultAction?: boolean;
}
export interface CatalogEntitySettingsMenu {

View File

@ -213,6 +213,6 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
* Getting default directory to download kubectl binaries
* @returns string
*/
export function getDefaultKubectlPath(): string {
export function getDefaultKubectlDownloadPath(): string {
return path.join((app || remote.app).getPath("userData"), "binaries");
}

View File

@ -21,6 +21,12 @@
import { action, ObservableMap } from "mobx";
export function multiSet<T, V>(map: Map<T, V>, newEntries: [T, V][]): void {
for (const [key, val] of newEntries) {
map.set(key, val);
}
}
export class ExtendedMap<K, V> extends Map<K, V> {
static new<K, V>(entries?: readonly (readonly [K, V])[] | null): ExtendedMap<K, V> {
return new ExtendedMap<K, V>(entries);

View File

@ -81,6 +81,7 @@ export async function bootstrap(App: AppComponent) {
initializers.initWelcomeMenuRegistry();
initializers.initWorkloadsOverviewDetailRegistry();
initializers.initCatalogEntityDetailRegistry();
initializers.initCatalogCategoryRegistryEntries();
initializers.initCatalog();
initializers.initIpcRendererListeners();

View File

@ -73,9 +73,10 @@ export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
@boundMethod
onButtonClick() {
if (this.menuItems.length == 1) {
this.menuItems[0].onClick();
}
const defaultAction = this.menuItems.find(item => item.defaultAction)?.onClick;
const clickAction = defaultAction || (this.menuItems.length === 1 ? this.menuItems[0].onClick : null);
clickAction?.();
}
render() {
@ -97,7 +98,7 @@ export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
{ this.menuItems.map((menuItem, index) => {
return <SpeedDialAction
key={index}
icon={<Icon material={menuItem.icon} />}
icon={<Icon material={menuItem.icon}/>}
tooltipTitle={menuItem.title}
onClick={() => menuItem.onClick()}
TooltipClasses={{

View File

@ -20,19 +20,17 @@
*/
import React from "react";
import { remote } from "electron";
import { Avatar, IconButton, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText, Paper } from "@material-ui/core";
import { Description, Folder, Delete, HelpOutline } from "@material-ui/icons";
import { action, computed, observable, reaction, makeObservable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import fse from "fs-extra";
import { KubeconfigSyncEntry, KubeconfigSyncValue, UserStore } from "../../../common/user-store";
import { Button } from "../button";
import { SubTitle } from "../layout/sub-title";
import { Spinner } from "../spinner";
import logger from "../../../main/logger";
import { iter } from "../../utils";
import { iter, multiSet } from "../../utils";
import { isWindows } from "../../../common/vars";
import { PathPicker } from "../path-picker/path-picker";
interface SyncInfo {
type: "file" | "folder" | "unknown";
@ -70,7 +68,9 @@ async function getMapEntry({ filePath, ...data}: KubeconfigSyncEntry): Promise<[
}
}
type SelectPathOptions = ("openFile" | "openDirectory")[];
export async function getAllEntries(filePaths: string[]): Promise<[string, Value][]> {
return Promise.all(filePaths.map(filePath => getMapEntry({ filePath })));
}
@observer
export class KubeconfigSyncs extends React.Component {
@ -109,24 +109,7 @@ export class KubeconfigSyncs extends React.Component {
}
@action
async openDialog(message: string, actions: SelectPathOptions) {
const { dialog, BrowserWindow } = remote;
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
properties: ["showHiddenFiles", "multiSelections", ...actions],
message,
buttonLabel: "Sync",
});
if (canceled) {
return;
}
const newEntries = await Promise.all(filePaths.map(filePath => getMapEntry({ filePath })));
for (const [filePath, info] of newEntries) {
this.syncs.set(filePath, info);
}
}
onPick = async (filePaths: string[]) => multiSet(this.syncs, await getAllEntries(filePaths));
renderEntryIcon(entry: Entry) {
switch (entry.info.type) {
@ -188,27 +171,30 @@ export class KubeconfigSyncs extends React.Component {
if (isWindows) {
return (
<div className="flex gaps align-center">
<Button
primary
<PathPicker
label="Sync file(s)"
className="box grow"
onClick={() => void this.openDialog("Sync file(s)", ["openFile"])}
onPick={this.onPick}
buttonLabel="Sync"
properties={["showHiddenFiles", "multiSelections", "openFile"]}
/>
<Button
primary
<PathPicker
label="Sync folder(s)"
className="box grow"
onClick={() => void this.openDialog("Sync folder(s)", ["openDirectory"])}
onPick={this.onPick}
buttonLabel="Sync"
properties={["showHiddenFiles", "multiSelections", "openDirectory"]}
/>
</div>
);
}
return (
<Button
primary
<PathPicker
label="Sync file(s) and folder(s)"
onClick={() => void this.openDialog("Sync file(s) and folder(s)", ["openFile", "openDirectory"])}
onPick={this.onPick}
buttonLabel="Sync"
properties={["showHiddenFiles", "multiSelections", "openFile", "openDirectory"]}
/>
);
}
@ -216,14 +202,8 @@ export class KubeconfigSyncs extends React.Component {
render() {
return (
<>
<section className="small">
<SubTitle title="Files and Folders to sync" />
{this.renderSyncButtons()}
<div className="hint">
Sync an individual file or all files in a folder (non-recursive).
</div>
{this.renderEntries()}
</section>
{this.renderSyncButtons()}
{this.renderEntries()}
</>
);
}

View File

@ -22,7 +22,7 @@
import React, { useState } from "react";
import { Input, InputValidators } from "../input";
import { SubTitle } from "../layout/sub-title";
import { getDefaultKubectlPath, UserStore } from "../../../common/user-store";
import { getDefaultKubectlDownloadPath, UserStore } from "../../../common/user-store";
import { observer } from "mobx-react";
import { bundledKubectlPath } from "../../../main/kubectl";
import { SelectOption, Select } from "../select";
@ -81,7 +81,7 @@ export const KubectlBinaries = observer(() => {
<Input
theme="round-black"
value={userStore.downloadBinariesPath}
placeholder={getDefaultKubectlPath()}
placeholder={getDefaultKubectlDownloadPath()}
validators={pathValidator}
onChange={setDownloadPath}
onBlur={save}

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { FileFilter, OpenDialogOptions, remote } from "electron";
import { observer } from "mobx-react";
import React from "react";
import { cssNames } from "../../utils";
import { Button } from "../button";
export interface PathPickOpts {
label: string;
onPick?: (paths: string[]) => any;
onCancel?: () => any;
defaultPath?: string;
buttonLabel?: string;
filters?: FileFilter[];
properties?: OpenDialogOptions["properties"];
securityScopedBookmarks?: boolean;
}
export interface PathPickerProps extends PathPickOpts {
className?: string;
disabled?: boolean;
}
@observer
export class PathPicker extends React.Component<PathPickerProps> {
static async pick(opts: PathPickOpts) {
const { onPick, onCancel, label, ...dialogOptions } = opts;
const { dialog, BrowserWindow } = remote;
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
message: label,
...dialogOptions,
});
if (canceled) {
await onCancel?.();
} else {
await onPick?.(filePaths);
}
}
async onClick() {
const { className, disabled, ...pickOpts } = this.props;
return PathPicker.pick(pickOpts);
}
render() {
const { className, label, disabled } = this.props;
return (
<Button
primary
label={label}
disabled={disabled}
className={cssNames("PathPicker", className)}
onClick={() => void this.onClick()}
/>
);
}
}

View File

@ -0,0 +1,95 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { kubernetesClusterCategory } from "../../common/catalog-entities";
import { addClusterURL } from "../../common/routes";
import { multiSet } from "../utils";
import { UserStore } from "../../common/user-store";
import { getAllEntries } from "../components/+preferences/kubeconfig-syncs";
import { runInAction } from "mobx";
import { isWindows } from "../../common/vars";
import { PathPicker } from "../components/path-picker/path-picker";
async function addSyncEntries(filePaths: string[]) {
const entries = await getAllEntries(filePaths);
runInAction(() => {
multiSet(UserStore.getInstance().syncKubeconfigEntries, entries);
});
}
export function initCatalogCategoryRegistryEntries() {
kubernetesClusterCategory.on("catalogAddMenu", ctx => {
ctx.menuItems.push(
{
icon: "text_snippet",
title: "Add from kubeconfig",
onClick: () => ctx.navigate(addClusterURL()),
},
);
if (isWindows) {
ctx.menuItems.push(
{
icon: "create_new_folder",
title: "Sync kubeconfig folders(s)",
defaultAction: true,
onClick: async () => {
await PathPicker.pick({
label: "Sync folders(s)",
buttonLabel: "Sync",
properties: ["showHiddenFiles", "multiSelections", "openDirectory"],
onPick: addSyncEntries,
});
},
},
{
icon: "note_add",
title: "Sync kubeconfig file(s)",
onClick: async () => {
await PathPicker.pick({
label: "Sync file(s)",
buttonLabel: "Sync",
properties: ["showHiddenFiles", "multiSelections", "openFile"],
onPick: addSyncEntries,
});
},
},
);
} else {
ctx.menuItems.push(
{
icon: "create_new_folder",
title: "Sync kubeconfig(s)",
defaultAction: true,
onClick: async () => {
await PathPicker.pick({
label: "Sync file(s)",
buttonLabel: "Sync",
properties: ["showHiddenFiles", "multiSelections", "openFile"],
onPick: addSyncEntries,
});
},
},
);
}
});
}

View File

@ -29,3 +29,4 @@ export * from "./kube-object-menu-registry";
export * from "./registries";
export * from "./welcome-menu-registry";
export * from "./workloads-overview-detail-registry";
export * from "./catalog-category-registry";