Migrating Vue components to React and stores refactoring (#585)

Signed-off-by: Roman <ixrock@gmail.com>

Co-authored-by: Sebastian Malton <sebastian@malton.name>
Co-authored-by: Sebastian Malton <smalton@mirantis.com>
Co-authored-by: Lauri Nevala <lauri.nevala@gmail.com>
Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
authored and GitHub committed 2020-08-20 08:53:07 +03:00
1 parent 905bbe9d3f
commit 5670312c47
297 files changed
+8186 -9103

No files matched your search

@@ -0,0 +1,12 @@
import type { IClusterViewRouteParams } from "../cluster-manager/cluster-view.route";
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
export interface IClusterSettingsRouteParams extends IClusterViewRouteParams {
}
export const clusterSettingsRoute: RouteProps = {
path: `/cluster/:clusterId/settings`,
}
export const clusterSettingsURL = buildURL<IClusterSettingsRouteParams>(clusterSettingsRoute.path)
@@ -0,0 +1,83 @@
.ClusterSettings {
.WizardLayout {
grid-template-columns: unset;
grid-template-rows: 76px 1fr;
padding: 0;
.head-col {
justify-content: space-between;
:nth-child(2) {
flex: 1 0 0;
}
}
.content-col {
margin: 0;
padding-top: $padding * 3;
background-color: transparent;
.SubTitle {
text-transform: none;
}
> div {
margin-top: $margin * 5;
}
.admin-note {
font-size: small;
opacity: 0.5;
margin-left: $margin;
}
.button-area {
margin-top: $margin * 2;
}
.file-loader {
margin-top: $margin * 2;
}
.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);
}
}
}
}
.Input, .Select {
margin-top: 10px;
}
.Select {
&__control {
box-shadow: 0 0 0 1px $borderFaintColor;
}
}
}
}
@@ -0,0 +1,43 @@
import "./cluster-settings.scss";
import React from "react";
import { Link } from "react-router-dom";
import { observer } from "mobx-react";
import { Features } from "./features";
import { Removal } from "./removal";
import { Status } from "./status";
import { General } from "./general";
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";
@observer
export class ClusterSettings extends React.Component {
render() {
const cluster = getMatchedCluster();
if (!cluster) return null;
const header = (
<>
<ClusterIcon
cluster={cluster}
showErrors={false}
showTooltip={false}
/>
<h2>{cluster.preferences.clusterName}</h2>
<Icon material="close" onClick={() => navigate("/")} 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>
);
}
}
@@ -0,0 +1,43 @@
import React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { Cluster } from "../../../../main/cluster";
import { Input } from "../../input";
import { SubTitle } from "../../layout/sub-title";
interface Props {
cluster: Cluster;
}
@observer
export class ClusterHomeDirSetting extends React.Component<Props> {
@observable directory = this.props.cluster.preferences.terminalCWD || "";
save = () => {
this.props.cluster.preferences.terminalCWD = this.directory;
};
onChange = (value: string) => {
this.directory = value;
}
render() {
return (
<>
<SubTitle title="Working Directory"/>
<p>Terminal working directory.</p>
<Input
theme="round-black"
value={this.directory}
onChange={this.onChange}
onBlur={this.save}
placeholder="$HOME"
/>
<span 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>
</>
);
}
}
@@ -0,0 +1,76 @@
import React from "react";
import { Cluster } from "../../../../main/cluster";
import { FilePicker, OverSizeLimitStyle } from "../../file-picker";
import { autobind } from "../../../utils";
import { Button } from "../../button";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { SubTitle } from "../../layout/sub-title";
import { ClusterIcon } from "../../cluster-icon";
enum GeneralInputStatus {
CLEAN = "clean",
ERROR = "error",
}
interface Props {
cluster: Cluster;
}
@observer
export class ClusterIconSetting extends React.Component<Props> {
@observable status = GeneralInputStatus.CLEAN;
@observable errorText?: string;
@autobind()
async onIconPick([file]: File[]) {
const { cluster } = this.props;
try {
if (file) {
const buf = Buffer.from(await file.arrayBuffer());
cluster.preferences.icon = `data:image/jpeg;base64, ${buf.toString('base64')}`;
} else {
// this has to be done as a seperate branch (and not always) because `cluster`
// is observable and triggers an update loop.
cluster.preferences.icon = undefined;
}
} catch (e) {
this.errorText = e.toString()
this.status = GeneralInputStatus.ERROR
}
}
getClearButton() {
if (this.props.cluster.preferences.icon) {
return <Button tooltip="Revert back to default icon" accent onClick={() => this.onIconPick([])}>Clear</Button>
}
}
render() {
const label = (
<>
<ClusterIcon
cluster={this.props.cluster}
showErrors={false}
showTooltip={false}
/>
{"Browse for new icon..."}
</>
);
return (
<>
<SubTitle title="Cluster Icon" />
<p>Define cluster icon. By default automatically generated.</p>
<div className="file-loader">
<FilePicker
accept="image/*"
label={label}
onOverSizeLimit={OverSizeLimitStyle.FILTER}
handler={this.onIconPick}
/>
{this.getClearButton()}
</div>
</>
);
}
}
@@ -0,0 +1,40 @@
import React from "react";
import { Cluster } from "../../../../main/cluster";
import { Input } from "../../input";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { SubTitle } from "../../layout/sub-title";
import { isRequired } from "../../input/input.validators";
interface Props {
cluster: Cluster;
}
@observer
export class ClusterNameSetting extends React.Component<Props> {
@observable name = this.props.cluster.preferences.clusterName || "";
save = () => {
this.props.cluster.preferences.clusterName = this.name;
};
onChange = (value: string) => {
this.name = value;
}
render() {
return (
<>
<SubTitle title="Cluster Name"/>
<p>Define cluster name.</p>
<Input
theme="round-black"
validators={isRequired}
value={this.name}
onChange={this.onChange}
onBlur={this.save}
/>
</>
);
}
}
@@ -0,0 +1,105 @@
import React from "react";
import { observer } from "mobx-react";
import { prometheusProviders } from "../../../../common/prometheus-providers";
import { Cluster } from "../../../../main/cluster";
import { SubTitle } from "../../layout/sub-title";
import { Select, SelectOption } from "../../select";
import { Input } from "../../input";
import { observable, computed } from "mobx";
const options: SelectOption<string>[] = [
{ value: "", label: "Auto detect" },
...prometheusProviders.map(pp => ({value: pp.id, label: pp.name}))
];
interface Props {
cluster: Cluster;
}
@observer
export class ClusterPrometheusSetting extends React.Component<Props> {
@observable path = "";
@observable provider = "";
@computed get canEditPrometheusPath() {
if (this.provider === "" || this.provider === "lens") return false;
return true;
}
componentDidMount() {
const { prometheus, prometheusProvider } = this.props.cluster.preferences;
if (prometheus) {
const prefix = prometheus.prefix || "";
this.path = `${prometheus.namespace}/${prometheus.service}:${prometheus.port}${prefix}`;
}
if (prometheusProvider) {
this.provider = prometheusProvider.type;
}
}
parsePrometheusPath = () => {
if (!this.provider || !this.path) {
return null;
}
const parsed = this.path.split(/\/|:/, 3);
const apiPrefix = this.path.substring(parsed.join("/").length);
if (!parsed[0] || !parsed[1] || !parsed[2]) {
return null;
}
return {
namespace: parsed[0],
service: parsed[1],
port: parseInt(parsed[2]),
prefix: apiPrefix
}
}
onSaveProvider = () => {
this.props.cluster.preferences.prometheusProvider = this.provider ?
{ type: this.provider } :
null;
}
onSavePath = () => {
this.props.cluster.preferences.prometheus = this.parsePrometheusPath();
};
render() {
return (
<>
<SubTitle title="Prometheus"/>
<p>
Use pre-installed Prometheus service for metrics. Please refer to the{" "}
<a href="https://github.com/lensapp/lens/blob/master/troubleshooting/custom-prometheus.md" target="_blank">guide</a>{" "}
for possible configuration changes.
</p>
<p>Prometheus installation method.</p>
<Select
value={this.provider}
onChange={({value}) => {
this.provider = value;
this.onSaveProvider();
}}
options={options}
/>
<span className="hint">What query format is used to fetch metrics from Prometheus</span>
{this.canEditPrometheusPath && (
<>
<p>Prometheus service address.</p>
<Input
theme="round-black"
value={this.path}
onChange={(value) => this.path = value}
onBlur={this.onSavePath}
placeholder="<namespace>/<service>:<port>"
/>
<span className="hint">
An address to an existing Prometheus installation{" "}
({'<namespace>/<service>:<port>'}). Lens tries to auto-detect address if left empty.
</span>
</>
)}
</>
);
}
}
@@ -0,0 +1,41 @@
import React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { Cluster } from "../../../../main/cluster";
import { Input } from "../../input";
import { isUrl } from "../../input/input.validators";
import { SubTitle } from "../../layout/sub-title";
interface Props {
cluster: Cluster;
}
@observer
export class ClusterProxySetting extends React.Component<Props> {
@observable proxy = this.props.cluster.preferences.httpsProxy || "";
save = () => {
this.props.cluster.preferences.httpsProxy = this.proxy;
};
onChange = (value: string) => {
this.proxy = value;
}
render() {
return (
<>
<SubTitle title="HTTP Proxy"/>
<p>HTTP Proxy server. Used for communicating with Kubernetes API.</p>
<Input
theme="round-black"
value={this.proxy}
onChange={this.onChange}
onBlur={this.save}
placeholder="http://<address>:<port>"
validators={isUrl}
/>
</>
);
}
}
@@ -0,0 +1,36 @@
import React from "react";
import { observer } from "mobx-react";
import { Link } from "react-router-dom";
import { workspacesURL } from "../../+workspaces";
import { workspaceStore } from "../../../../common/workspace-store";
import { Cluster } from "../../../../main/cluster";
import { Select } from "../../../components/select";
import { SubTitle } from "../../layout/sub-title";
interface Props {
cluster: Cluster;
}
@observer
export class ClusterWorkspaceSetting extends React.Component<Props> {
render() {
return (
<>
<SubTitle title="Cluster Workspace"/>
<p>
Define cluster{" "}
<Link to={workspacesURL()}>
workspace
</Link>.
</p>
<Select
value={this.props.cluster.workspace}
onChange={({value}) => this.props.cluster.workspace = value}
options={workspaceStore.workspacesList.map(w =>
({value: w.id, label: w.name})
)}
/>
</>
);
}
}
@@ -0,0 +1,93 @@
import React from "react";
import { observable, reaction, comparer } from "mobx";
import { observer, disposeOnUnmount } from "mobx-react";
import { clusterIpc } from "../../../../common/cluster-ipc";
import { Cluster } from "../../../../main/cluster";
import { Button } from "../../button";
import { Notifications } from "../../notifications";
import { Spinner } from "../../spinner";
interface Props {
cluster: Cluster
feature: string
}
@observer
export class InstallFeature extends React.Component<Props> {
@observable loading = false;
componentDidMount() {
disposeOnUnmount(this,
reaction(() => this.props.cluster.features[this.props.feature], () => {
this.loading = false;
}, { equals: comparer.structural })
);
}
getActionButtons() {
const { cluster, feature } = this.props;
const features = cluster.features[feature];
const disabled = !cluster.isAdmin || this.loading;
const loadingIcon = this.loading ? <Spinner/> : null;
if (!features) return null;
return (
<div className="flex gaps align-center">
{features.canUpgrade &&
<Button
primary
disabled={disabled}
onClick={this.runAction(() =>
clusterIpc.upgradeFeature.invokeFromRenderer(cluster.id, feature))
}
>
Upgrade
</Button>
}
{features.installed &&
<Button
accent
disabled={disabled}
onClick={this.runAction(() =>
clusterIpc.uninstallFeature.invokeFromRenderer(cluster.id, feature))
}
>
Uninstall
</Button>
}
{!features.installed && !features.canUpgrade &&
<Button
primary
disabled={disabled}
onClick={this.runAction(() =>
clusterIpc.installFeature.invokeFromRenderer(cluster.id, feature))
}
>
Install
</Button>
}
{loadingIcon}
{!cluster.isAdmin && <span className='admin-note'>Actions can only be performed by admins.</span>}
</div>
);
}
runAction(action: () => Promise<any>): () => Promise<void> {
return async () => {
try {
this.loading = true;
await action();
} catch (err) {
Notifications.error(err.toString());
}
};
}
render() {
return (
<>
{this.props.children}
<div className="button-area">{this.getActionButtons()}</div>
</>
);
}
}
@@ -0,0 +1,37 @@
import React from "react";
import { Trans } from "@lingui/macro";
import { observer } from "mobx-react";
import { clusterIpc } from "../../../../common/cluster-ipc";
import { clusterStore } from "../../../../common/cluster-store";
import { Cluster } from "../../../../main/cluster";
import { autobind } from "../../../utils";
import { Button } from "../../button";
import { ConfirmDialog } from "../../confirm-dialog";
interface Props {
cluster: Cluster;
}
@observer
export class RemoveClusterButton extends React.Component<Props> {
@autobind()
confirmRemoveCluster() {
const { cluster } = this.props;
ConfirmDialog.open({
message: <p>Are you sure you want to remove <b>{cluster.preferences.clusterName}</b> from Lens?</p>,
labelOk: <Trans>Yes</Trans>,
labelCancel: <Trans>No</Trans>,
ok: async () => {
await clusterStore.removeById(cluster.id);
}
})
}
render() {
return (
<Button accent onClick={this.confirmRemoveCluster} className="button-area">
Remove Cluster
</Button>
);
}
}
@@ -0,0 +1,42 @@
import React from "react";
import { Cluster } from "../../../main/cluster";
import { InstallFeature } from "./components/install-feature";
import { SubTitle } from "../layout/sub-title";
import { MetricsFeature } from "../../../features/metrics";
import { UserModeFeature } from "../../../features/user-mode";
interface Props {
cluster: Cluster;
}
export class Features extends React.Component<Props> {
render() {
const { cluster } = this.props;
return (
<div>
<h2>Features</h2>
<InstallFeature cluster={cluster} feature={MetricsFeature.id}>
<>
<SubTitle title="Metrics"/>
<p>
Enable timeseries data visualization (Prometheus stack) for your cluster.
Install this only if you don't have existing Prometheus stack installed.
You can see preview of manifests{" "}
<a href="https://github.com/lensapp/lens/tree/master/src/features/metrics" target="_blank">here</a>.
</p>
</>
</InstallFeature>
<InstallFeature cluster={cluster} feature={UserModeFeature.id}>
<>
<SubTitle title="User Mode"/>
<p>
User Mode feature enables non-admin users to see namespaces they have access to.{" "}
This is achieved by configuring RBAC rules so that every authenticated user is granted to list namespaces.
</p>
</>
</InstallFeature>
</div>
);
}
}
@@ -0,0 +1,26 @@
import React from "react";
import { Cluster } from "../../../main/cluster";
import { ClusterNameSetting } from "./components/cluster-name-setting";
import { ClusterWorkspaceSetting } from "./components/cluster-workspace-setting";
import { ClusterIconSetting } from "./components/cluster-icon-setting";
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
interface Props {
cluster: Cluster;
}
export class General extends React.Component<Props> {
render() {
return <div>
<h2>General</h2>
<ClusterNameSetting cluster={this.props.cluster} />
<ClusterWorkspaceSetting cluster={this.props.cluster} />
<ClusterIconSetting cluster={this.props.cluster} />
<ClusterProxySetting cluster={this.props.cluster} />
<ClusterPrometheusSetting cluster={this.props.cluster} />
<ClusterHomeDirSetting cluster={this.props.cluster} />
</div>;
}
}
@@ -0,0 +1,2 @@
export * from "./cluster-settings.route"
export * from "./cluster-settings"
@@ -0,0 +1,20 @@
import React from "react";
import { Cluster } from "../../../main/cluster";
import { RemoveClusterButton } from "./components/remove-cluster-button";
interface Props {
cluster: Cluster;
}
export class Removal extends React.Component<Props> {
render() {
const { cluster } = this.props;
return (
<div>
<h2>Removal</h2>
<RemoveClusterButton cluster={cluster} />
</div>
);
}
}
@@ -0,0 +1,46 @@
import React from "react";
import { Cluster } from "../../../main/cluster";
import { SubTitle } from "../layout/sub-title";
import { Table, TableCell, TableRow } from "../table";
interface Props {
cluster: Cluster;
}
export class Status extends React.Component<Props> {
renderStatusRows() {
const { cluster } = this.props;
const rows = [
["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"})`],
["Distribution", cluster.distribution],
["Kerbel Version", cluster.version],
["API Address", cluster.apiUrl],
["Nodes Count", cluster.nodes || "0"]
];
return (
<Table scrollable={false}>
{rows.map(([name, value]) => {
return (
<TableRow key={name}>
<TableCell>{name}</TableCell>
<TableCell className="value">{value}</TableCell>
</TableRow>
);
})}
</Table>
);
}
render() {
return <div>
<h2>Status</h2>
<SubTitle title="Cluster Status"/>
<p>
Cluster status information including: detected distribution, kernel version, and online status.
</p>
<div className="status-table">
{this.renderStatusRows()}
</div>
</div>;
}
}