1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/src/renderer/components/+preferences/kubeconfig-syncs.tsx
Sebastian Malton 998f7aa934
Add the ability to sync kube config files (#2567)
* Add the ability to sync kube config files

- Will update when the files changes

- add KUBECONFIG_SYNC label

- fix rebase and change to addObservableSource

- move UI to user settings

- support shallow folder watching

- add some unit tests for the diff-er

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* responding to review comments

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix tests and add try/catch

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* always sync c&p folder, remove bad rebase

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix preferences

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix settings saving and catalog view

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix unit tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix synced clusters not connectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* change to non-complete shallow watching

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix sizing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Catch readStream errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* don't clear UserStore on non-existant preference field, fix unlinking not removing items from source

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* change label to file

Signed-off-by: Sebastian Malton <sebastian@malton.name>
2021-04-30 16:48:20 +03:00

177 lines
4.7 KiB
TypeScript

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 } 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";
interface SyncInfo {
type: "file" | "folder" | "unknown";
}
interface Entry extends Value {
filePath: string;
}
interface Value {
data: KubeconfigSyncValue;
info: SyncInfo;
}
async function getMapEntry({ filePath, ...data}: KubeconfigSyncEntry): Promise<[string, Value]> {
try {
// stat follows the stat(2) linux syscall spec, namely it follows symlinks
const stats = await fse.stat(filePath);
if (stats.isFile()) {
return [filePath, { info: { type: "file" }, data }];
}
if (stats.isDirectory()) {
return [filePath, { info: { type: "folder" }, data }];
}
logger.warn("[KubeconfigSyncs]: unknown stat entry", { stats });
return [filePath, { info: { type: "unknown" }, data }];
} catch (error) {
logger.warn(`[KubeconfigSyncs]: failed to stat entry: ${error}`, { error });
return [filePath, { info: { type: "unknown" }, data }];
}
}
@observer
export class KubeconfigSyncs extends React.Component {
syncs = observable.map<string, Value>();
@observable loaded = false;
async componentDidMount() {
const mapEntries = await Promise.all(
iter.map(
UserStore.getInstance().syncKubeconfigEntries,
([filePath, ...value]) => getMapEntry({ filePath, ...value }),
),
);
this.syncs.replace(mapEntries);
this.loaded = true;
disposeOnUnmount(this, [
reaction(() => Array.from(this.syncs.entries(), ([filePath, { data }]) => [filePath, data]), syncs => {
UserStore.getInstance().syncKubeconfigEntries.replace(syncs);
})
]);
}
@computed get syncsList(): Entry[] | undefined {
if (!this.loaded) {
return undefined;
}
return Array.from(this.syncs.entries(), ([filePath, value]) => ({ filePath, ...value }));
}
@action
openFileDialog = async () => {
const { dialog, BrowserWindow } = remote;
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
properties: ["openFile", "showHiddenFiles", "multiSelections", "openDirectory"],
message: "Select kubeconfig file(s) and folder(s)",
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);
}
};
renderEntryIcon(entry: Entry) {
switch (entry.info.type) {
case "file":
return <Description />;
case "folder":
return <Folder />;
case "unknown":
return <HelpOutline />;
}
}
renderEntry = (entry: Entry) => {
return (
<Paper className="entry" key={entry.filePath} elevation={3}>
<ListItem>
<ListItemAvatar>
<Avatar>
{this.renderEntryIcon(entry)}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={entry.filePath}
className="description"
/>
<ListItemSecondaryAction className="action">
<IconButton
edge="end"
aria-label="delete"
onClick={() => this.syncs.delete(entry.filePath)}
>
<Delete />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</Paper>
);
};
renderEntries() {
const entries = this.syncsList;
if (!entries) {
return (
<div className="loading-spinner">
<Spinner />
</div>
);
}
return (
<List className="kubeconfig-sync-list">
{entries.map(this.renderEntry)}
</List>
);
}
render() {
return (
<>
<section className="small">
<SubTitle title="Files and Folders to sync" />
<Button
primary
label="Sync file or folder"
onClick={() => void this.openFileDialog()}
/>
<div className="hint">
Sync an individual file or all files in a folder (non-recursive).
</div>
{this.renderEntries()}
</section>
</>
);
}
}