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:
parent
14f8343170
commit
f589987e19
@ -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);
|
||||
|
||||
@ -123,6 +123,7 @@ export interface CatalogEntityContextMenu {
|
||||
|
||||
export interface CatalogEntityAddMenu extends CatalogEntityContextMenu {
|
||||
icon: string;
|
||||
defaultAction?: boolean;
|
||||
}
|
||||
|
||||
export interface CatalogEntitySettingsMenu {
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -81,6 +81,7 @@ export async function bootstrap(App: AppComponent) {
|
||||
initializers.initWelcomeMenuRegistry();
|
||||
initializers.initWorkloadsOverviewDetailRegistry();
|
||||
initializers.initCatalogEntityDetailRegistry();
|
||||
initializers.initCatalogCategoryRegistryEntries();
|
||||
initializers.initCatalog();
|
||||
initializers.initIpcRendererListeners();
|
||||
|
||||
|
||||
@ -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={{
|
||||
|
||||
@ -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()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
80
src/renderer/components/path-picker/path-picker.tsx
Normal file
80
src/renderer/components/path-picker/path-picker.tsx
Normal 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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
95
src/renderer/initializers/catalog-category-registry.tsx
Normal file
95
src/renderer/initializers/catalog-category-registry.tsx
Normal 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,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -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";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user