mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fixing Cluster Settings layout (#651)
* A bit of cleaning in Add Cluster page Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Adding head-col to WizardLayout Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Fixing Cluster Settings general layout bugs Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Cluster Status view refactoring Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Install Metrics component refactoring Using notifications for error, removed picking button icon method, simplified button generation. Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Remove icons / checks from RemoveClusterButton Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Fixing colorError in Input styles Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Preventing Input's spellchecking Signed-off-by: alexfront <alex.andreev.email@gmail.com> * ClusterNameSettings refactoring Signed-off-by: alexfront <alex.andreev.email@gmail.com> * ClusterWorkspaceSettings refactoring/fixing Signed-off-by: alexfront <alex.andreev.email@gmail.com> * ClusterProxySetting refactoring Signed-off-by: alexfront <alex.andreev.email@gmail.com> * ClusterPrometheusSetting refactoring Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Clean up Removal section Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Glued InstallMetrics & InstallUserMode into 1 component Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Removing unused styles in Cluster Settings Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Cluster Settings styling Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Adding close button to settings header Signed-off-by: alexfront <alex.andreev.email@gmail.com> * ClusterHomeDirSetting refactoring Signed-off-by: alexfront <alex.andreev.email@gmail.com> * FilePicker restyling Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Fixing Prometheus selector Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Fixing Hashicon Passing cluster name instead of cluster id to prevent icon changing while typing new cluster name Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Minor ClusterSettings fixes Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Increasing opacity for non-interactive icons Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Keep feature install loading state Waiting for props to change before disabling loading state (gray button width spinner) Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Remove arrays in disposeOnUnmount() Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Fix Cluster select behavior Now clicking cluster icon in sidebar always leads to / dashboard. And 'Settings' submenu switches active cluster at first and only the showing Cluster Settings Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Using structuralComparator in feature installer Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Saving input fields on blur Signed-off-by: alexfront <alex.andreev.email@gmail.com> * Setting Select color same as Input color Signed-off-by: alexfront <alex.andreev.email@gmail.com>
This commit is contained in:
parent
1f5acdb9cd
commit
0f4248de68
@ -86,6 +86,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
return Array.from(this.clusters.values());
|
return Array.from(this.clusters.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActive(id: ClusterId) {
|
||||||
|
this.activeClusterId = id;
|
||||||
|
}
|
||||||
|
|
||||||
hasClusters() {
|
hasClusters() {
|
||||||
return this.clusters.size > 0;
|
return this.clusters.size > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -124,7 +124,7 @@ export class AddCluster extends React.Component {
|
|||||||
to allow you to operate easily on multiple clusters and/or contexts.
|
to allow you to operate easily on multiple clusters and/or contexts.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
For more information on kubeconfig see <a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/" target="_blank">Kubernetes docs</a>
|
For more information on kubeconfig see <a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/" target="_blank">Kubernetes docs</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
NOTE: Any manually added cluster is not merged into your kubeconfig file.
|
NOTE: Any manually added cluster is not merged into your kubeconfig file.
|
||||||
@ -137,13 +137,12 @@ export class AddCluster extends React.Component {
|
|||||||
app.
|
app.
|
||||||
</p>
|
</p>
|
||||||
<a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#option-1-oidc-authenticator" target="_blank">
|
<a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#option-1-oidc-authenticator" target="_blank">
|
||||||
<h4>OIDC (OpenID Connect)</h4>
|
<h3>OIDC (OpenID Connect)</h3>
|
||||||
</a>
|
</a>
|
||||||
<div>
|
|
||||||
<p>
|
<p>
|
||||||
When connecting Lens to OIDC enabled cluster, there's few things you as a user need to take into account.
|
When connecting Lens to OIDC enabled cluster, there's few things you as a user need to take into account.
|
||||||
</p>
|
</p>
|
||||||
<b>Dedicated refresh token</b>
|
<p><b>Dedicated refresh token</b></p>
|
||||||
<p>
|
<p>
|
||||||
As Lens app utilized kubeconfig is "disconnected" from your main kubeconfig Lens needs to have it's own refresh token it utilizes.
|
As Lens app utilized kubeconfig is "disconnected" from your main kubeconfig Lens needs to have it's own refresh token it utilizes.
|
||||||
If you share the refresh token with e.g. <code>kubectl</code> who ever uses the token first will invalidate it for the next user.
|
If you share the refresh token with e.g. <code>kubectl</code> who ever uses the token first will invalidate it for the next user.
|
||||||
@ -151,8 +150,7 @@ export class AddCluster extends React.Component {
|
|||||||
(both <code>id_token</code> and <code>refresh_token</code>) from
|
(both <code>id_token</code> and <code>refresh_token</code>) from
|
||||||
the config and issuing <code>kubelogin</code> command. That'll take you through the login process and will result you having "dedicated" refresh token.
|
the config and issuing <code>kubelogin</code> command. That'll take you through the login process and will result you having "dedicated" refresh token.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<h3>Exec auth plugins</h3>
|
||||||
<h4>Exec auth plugins</h4>
|
|
||||||
<p>
|
<p>
|
||||||
When using <a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuration" target="_blank">exec auth</a> plugins make sure the paths that are used to call
|
When using <a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuration" target="_blank">exec auth</a> plugins make sure the paths that are used to call
|
||||||
any binaries
|
any binaries
|
||||||
|
|||||||
@ -1,86 +1,85 @@
|
|||||||
.ClusterSettings {
|
.ClusterSettings {
|
||||||
overflow-y: scroll;
|
|
||||||
grid-template-columns: unset;
|
grid-template-columns: unset;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.head-col {
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
:nth-child(2) {
|
||||||
|
flex: 1 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: $grey-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.info-col {
|
.info-col {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-col {
|
.content-col {
|
||||||
margin-right: unset;
|
margin: 0;
|
||||||
|
padding-top: $padding * 3;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
.SubTitle {
|
||||||
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
.settings-wrapper {
|
||||||
margin-top: 40px;
|
margin: 0 auto;
|
||||||
|
width: 60%;
|
||||||
|
min-width: 570px;
|
||||||
|
max-width: 1000px;
|
||||||
|
|
||||||
&:first-child {
|
> div {
|
||||||
margin-top: 0px;
|
margin-top: $margin * 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-note {
|
||||||
|
font-size: small;
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-left: $margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-area {
|
||||||
|
margin-top: $margin * 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
.file-loader {
|
||||||
margin-top: 20px;
|
margin-top: $margin * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: smaller;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-table {
|
.status-table {
|
||||||
margin-top: 20px;
|
margin: $margin * 3 0;
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 3fr;
|
.Table {
|
||||||
grid-gap: 10px;
|
border: 1px solid var(--drawerSubtitleBackground);
|
||||||
|
border-radius: $radius;
|
||||||
|
|
||||||
|
.TableRow {
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
border-bottom: 1px solid var(--drawerSubtitleBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.value {
|
||||||
margin-top: 20px;
|
flex-grow: 2;
|
||||||
text-align: center;
|
color: var(--textColorSecondary);
|
||||||
|
}
|
||||||
.Spinner {
|
}
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Input,.Select {
|
.Input,.Select {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Icon:not(.updated):not(.clean) {
|
|
||||||
color: #ad0000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Icon.updated {
|
|
||||||
color: #00dd1d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.updated {
|
|
||||||
animation: updated-name 1s 1;
|
|
||||||
animation-fill-mode: forwards;
|
|
||||||
animation-delay: 3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes updated-name {
|
|
||||||
from {opacity :1;}
|
|
||||||
to {opacity :0;}
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"]::placeholder {
|
|
||||||
font-size: small;
|
|
||||||
color: #707070;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"] {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
.Spinner {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-color: transparent black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,25 +1,43 @@
|
|||||||
import "./cluster-settings.scss"
|
import "./cluster-settings.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Features } from "./features"
|
import { Features } from "./features";
|
||||||
import { Removal } from "./removal"
|
import { Removal } from "./removal";
|
||||||
import { Status } from "./status"
|
import { Status } from "./status";
|
||||||
import { General } from "./general"
|
import { General } from "./general";
|
||||||
import { getHostedCluster } from "../../../common/cluster-store"
|
import { getHostedCluster } from "../../../common/cluster-store";
|
||||||
import { WizardLayout } from "../layout/wizard-layout";
|
import { WizardLayout } from "../layout/wizard-layout";
|
||||||
|
import { ClusterIcon } from "../cluster-icon";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterSettings extends React.Component {
|
export class ClusterSettings extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const cluster = getHostedCluster();
|
const cluster = getHostedCluster();
|
||||||
|
const header = (
|
||||||
|
<>
|
||||||
|
<ClusterIcon
|
||||||
|
cluster={cluster}
|
||||||
|
showErrors={false}
|
||||||
|
showTooltip={false}
|
||||||
|
/>
|
||||||
|
<h2>{cluster.preferences.clusterName}</h2>
|
||||||
|
<Link to="/">
|
||||||
|
<Icon material="close" big />
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<WizardLayout className="ClusterSettings">
|
<WizardLayout header={header} className="ClusterSettings">
|
||||||
|
<div className="settings-wrapper">
|
||||||
<Status cluster={cluster}></Status>
|
<Status cluster={cluster}></Status>
|
||||||
<General cluster={cluster}></General>
|
<General cluster={cluster}></General>
|
||||||
<Features cluster={cluster}></Features>
|
<Features cluster={cluster}></Features>
|
||||||
<Removal cluster={cluster}></Removal>
|
<Removal cluster={cluster}></Removal>
|
||||||
|
</div>
|
||||||
</WizardLayout>
|
</WizardLayout>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Cluster } from "../../../../main/cluster";
|
|
||||||
import { Input } from "../../input";
|
|
||||||
import { Spinner } from "../../spinner";
|
|
||||||
import { clusterStore } from "../../../../common/cluster-store"
|
|
||||||
import { Icon } from "../../icon";
|
|
||||||
import { Tooltip, TooltipPosition } from "../../tooltip";
|
|
||||||
import { autobind } from "../../../utils";
|
|
||||||
import { TextInputStatus } from "./statuses"
|
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { Cluster } from "../../../../main/cluster";
|
||||||
|
import { Input } from "../../input";
|
||||||
|
import { SubTitle } from "../../layout/sub-title";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -17,70 +12,32 @@ interface Props {
|
|||||||
@observer
|
@observer
|
||||||
export class ClusterHomeDirSetting extends React.Component<Props> {
|
export class ClusterHomeDirSetting extends React.Component<Props> {
|
||||||
@observable directory = this.props.cluster.preferences.terminalCWD || "";
|
@observable directory = this.props.cluster.preferences.terminalCWD || "";
|
||||||
@observable status = TextInputStatus.CLEAN;
|
|
||||||
@observable errorText?: string;
|
save = () => {
|
||||||
|
this.props.cluster.preferences.terminalCWD = this.directory;
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange = (value: string) => {
|
||||||
|
this.directory = value;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <>
|
return (
|
||||||
<h4>Working Directory</h4>
|
<>
|
||||||
<p>Set initial working directory for terminals. When set it will the `pwd` when a new terminal instance is opened for this cluster.</p>
|
<SubTitle title="Working Directory"/>
|
||||||
|
<p>Terminal working directory.</p>
|
||||||
<Input
|
<Input
|
||||||
theme="round-black"
|
theme="round-black"
|
||||||
className="box grow"
|
|
||||||
value={this.directory}
|
value={this.directory}
|
||||||
onSubmit={this.onWorkingDirectorySubmit}
|
onChange={this.onChange}
|
||||||
onChange={this.onWorkingDirectoryChange}
|
onBlur={this.save}
|
||||||
iconRight={this.getIconRight()}
|
|
||||||
placeholder="$HOME"
|
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.
|
||||||
@autobind()
|
</span>
|
||||||
onWorkingDirectoryChange(directory: string, _e: React.ChangeEvent) {
|
</>
|
||||||
if (this.status === TextInputStatus.UPDATING) {
|
);
|
||||||
console.log("prevent changing cluster directory while updating");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.status = this.dirDiffers(directory);
|
|
||||||
this.directory = directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
dirDiffers(directory: string): TextInputStatus {
|
|
||||||
const { terminalCWD = "" } = this.props.cluster.preferences;
|
|
||||||
|
|
||||||
return directory === terminalCWD ? TextInputStatus.CLEAN : TextInputStatus.DIRTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
getIconRight(): React.ReactNode {
|
|
||||||
switch (this.status) {
|
|
||||||
case TextInputStatus.CLEAN:
|
|
||||||
return null;
|
|
||||||
case TextInputStatus.DIRTY:
|
|
||||||
return <Icon size="16px" material="fiber_manual_record"/>;
|
|
||||||
case TextInputStatus.UPDATED:
|
|
||||||
return <Icon size="16px" className="updated" material="done"/>;
|
|
||||||
case TextInputStatus.UPDATING:
|
|
||||||
return <Spinner />;
|
|
||||||
case TextInputStatus.ERROR:
|
|
||||||
return <Icon id="cluster-directory-setting-error-icon" size="16px" material="error">
|
|
||||||
<Tooltip targetId="cluster-directory-setting-error-icon" position={TooltipPosition.TOP}>
|
|
||||||
{this.errorText}
|
|
||||||
</Tooltip>
|
|
||||||
</Icon>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
onWorkingDirectorySubmit(directory: string) {
|
|
||||||
if (this.dirDiffers(directory) !== TextInputStatus.DIRTY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.status = TextInputStatus.UPDATING
|
|
||||||
this.props.cluster.preferences.terminalCWD = directory;
|
|
||||||
this.directory = directory;
|
|
||||||
this.status = TextInputStatus.UPDATED
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,13 +1,17 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Cluster } from "../../../../main/cluster";
|
import { Cluster } from "../../../../main/cluster";
|
||||||
import { clusterStore } from "../../../../common/cluster-store"
|
|
||||||
import { Icon } from "../../icon";
|
|
||||||
import { FilePicker, OverSizeLimitStyle } from "../../file-picker";
|
import { FilePicker, OverSizeLimitStyle } from "../../file-picker";
|
||||||
import { autobind } from "../../../utils";
|
import { autobind } from "../../../utils";
|
||||||
import { Button } from "../../button";
|
import { Button } from "../../button";
|
||||||
import { GeneralInputStatus } from "./statuses"
|
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { SubTitle } from "../../layout/sub-title";
|
||||||
|
import { ClusterIcon } from "../../cluster-icon";
|
||||||
|
|
||||||
|
enum GeneralInputStatus {
|
||||||
|
CLEAN = "clean",
|
||||||
|
ERROR = "error",
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -21,7 +25,6 @@ export class ClusterIconSetting extends React.Component<Props> {
|
|||||||
@autobind()
|
@autobind()
|
||||||
async onIconPick([file]: File[]) {
|
async onIconPick([file]: File[]) {
|
||||||
const { cluster } = this.props;
|
const { cluster } = this.props;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (file) {
|
if (file) {
|
||||||
const buf = Buffer.from(await file.arrayBuffer());
|
const buf = Buffer.from(await file.arrayBuffer());
|
||||||
@ -38,35 +41,36 @@ export class ClusterIconSetting extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getClearButton() {
|
getClearButton() {
|
||||||
const { cluster } = this.props;
|
if (this.props.cluster.preferences.icon) {
|
||||||
|
return <Button tooltip="Revert back to default icon" accent onClick={() => this.onIconPick([])}>Clear</Button>
|
||||||
if (cluster.preferences.icon) {
|
|
||||||
return <Button accent onClick={() => this.onIconPick([])}>Clear</Button>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <>
|
const label = (
|
||||||
<h4>Cluster Icon</h4>
|
<>
|
||||||
<p>Set cluster icon. By default it is automatically generated. {this.getIconRight()}</p>
|
<ClusterIcon
|
||||||
<div className="center">
|
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
|
<FilePicker
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
labelText="Browse for new icon..."
|
label={label}
|
||||||
onOverSizeLimit={OverSizeLimitStyle.FILTER}
|
onOverSizeLimit={OverSizeLimitStyle.FILTER}
|
||||||
handler={this.onIconPick}
|
handler={this.onIconPick}
|
||||||
/>
|
/>
|
||||||
{this.getClearButton()}
|
{this.getClearButton()}
|
||||||
</div>
|
</div>
|
||||||
</>;
|
</>
|
||||||
}
|
);
|
||||||
|
|
||||||
getIconRight(): React.ReactNode {
|
|
||||||
switch (this.status) {
|
|
||||||
case GeneralInputStatus.CLEAN:
|
|
||||||
return null;
|
|
||||||
case GeneralInputStatus.ERROR:
|
|
||||||
return <Icon size="16px" material="error" title={this.errorText}></Icon>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,14 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Cluster } from "../../../../main/cluster";
|
import { Cluster } from "../../../../main/cluster";
|
||||||
import { Input } from "../../input";
|
import { Input } from "../../input";
|
||||||
import { Spinner } from "../../spinner";
|
|
||||||
import { clusterStore } from "../../../../common/cluster-store"
|
|
||||||
import { Icon } from "../../icon";
|
|
||||||
import { Tooltip, TooltipPosition } from "../../tooltip";
|
|
||||||
import { autobind } from "../../../utils";
|
|
||||||
import { TextInputStatus } from "./statuses"
|
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { SubTitle } from "../../layout/sub-title";
|
||||||
|
import { isRequired } from "../../input/input.validators";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -17,69 +13,28 @@ interface Props {
|
|||||||
@observer
|
@observer
|
||||||
export class ClusterNameSetting extends React.Component<Props> {
|
export class ClusterNameSetting extends React.Component<Props> {
|
||||||
@observable name = this.props.cluster.preferences.clusterName || "";
|
@observable name = this.props.cluster.preferences.clusterName || "";
|
||||||
@observable status = TextInputStatus.CLEAN;
|
|
||||||
@observable errorText?: string;
|
save = () => {
|
||||||
|
this.props.cluster.preferences.clusterName = this.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange = (value: string) => {
|
||||||
|
this.name = value;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <>
|
return (
|
||||||
<h4>Cluster Name</h4>
|
<>
|
||||||
<p>Change cluster name:</p>
|
<SubTitle title="Cluster Name"/>
|
||||||
|
<p>Define cluster name.</p>
|
||||||
<Input
|
<Input
|
||||||
theme="round-black"
|
theme="round-black"
|
||||||
className="box grow"
|
validators={isRequired}
|
||||||
value={this.name}
|
value={this.name}
|
||||||
onSubmit={this.onClusterNameSubmit}
|
onChange={this.onChange}
|
||||||
onChange={this.onClusterNameChange}
|
onBlur={this.save}
|
||||||
iconRight={this.getIconRight()}
|
|
||||||
/>
|
/>
|
||||||
</>;
|
</>
|
||||||
}
|
);
|
||||||
|
|
||||||
@autobind()
|
|
||||||
onClusterNameChange(name: string, _e: React.ChangeEvent) {
|
|
||||||
if (this.status === TextInputStatus.UPDATING) {
|
|
||||||
console.log("prevent changing cluster name while updating");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.status = this.nameDiffers(name)
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
nameDiffers(name: string): TextInputStatus {
|
|
||||||
const { clusterName } = this.props.cluster.preferences;
|
|
||||||
|
|
||||||
return name === clusterName ? TextInputStatus.CLEAN : TextInputStatus.DIRTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
getIconRight(): React.ReactNode {
|
|
||||||
switch (this.status) {
|
|
||||||
case TextInputStatus.CLEAN:
|
|
||||||
return null;
|
|
||||||
case TextInputStatus.DIRTY:
|
|
||||||
return <Icon size="16px" material="fiber_manual_record"/>;
|
|
||||||
case TextInputStatus.UPDATED:
|
|
||||||
return <Icon size="16px" className="updated" material="done"/>;
|
|
||||||
case TextInputStatus.UPDATING:
|
|
||||||
return <Spinner/>;
|
|
||||||
case TextInputStatus.ERROR:
|
|
||||||
return <Icon id="cluster-name-setting-error-icon" size="16px" material="error">
|
|
||||||
<Tooltip targetId="cluster-name-setting-error-icon" position={TooltipPosition.TOP}>
|
|
||||||
{this.errorText}
|
|
||||||
</Tooltip>
|
|
||||||
</Icon>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
onClusterNameSubmit(name: string) {
|
|
||||||
if (this.nameDiffers(name) !== TextInputStatus.DIRTY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.status = TextInputStatus.UPDATING
|
|
||||||
this.props.cluster.preferences.clusterName = name;
|
|
||||||
this.name = name;
|
|
||||||
this.status = TextInputStatus.UPDATED
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,13 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Cluster } from "../../../../main/cluster";
|
import merge from "lodash/merge";
|
||||||
import { clusterStore } from "../../../../common/cluster-store"
|
|
||||||
import { Select, SelectOption, SelectProps } from "../../select";
|
|
||||||
import { prometheusProviders } from "../../../../common/prometheus-providers";
|
|
||||||
import { autobind } from "../../../utils";
|
|
||||||
import { observable } from "mobx";
|
|
||||||
import { observer } from "mobx-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";
|
||||||
|
|
||||||
const prometheusGuide = "https://github.com/lensapp/lens/blob/master/troubleshooting/custom-prometheus.md";
|
|
||||||
const options: SelectOption<string>[] = [
|
const options: SelectOption<string>[] = [
|
||||||
{ value: "", label: "Auto detect" },
|
{ value: "", label: "Auto detect" },
|
||||||
...prometheusProviders.map(pp => ({value: pp.id, label: pp.name}))
|
...prometheusProviders.map(pp => ({value: pp.id, label: pp.name}))
|
||||||
@ -19,23 +17,28 @@ interface Props {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterPrometheusSetting extends React.Component<Props> {
|
export class ClusterPrometheusSetting extends React.Component<Props> {
|
||||||
@observable prometheusProvider = this.props.cluster.preferences.prometheusProvider?.type || "";
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <>
|
return (
|
||||||
<h4>Cluster Prometheus</h4>
|
<>
|
||||||
<p>Use pre-installed Prometheus service for metrics. Please refer to <a href={prometheusGuide}>this guide</a> for possible configuration changes.</p>
|
<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>
|
||||||
<Select
|
<Select
|
||||||
value={this.prometheusProvider}
|
value={this.props.cluster.preferences.prometheusProvider?.type || ""}
|
||||||
|
onChange={({value}) => {
|
||||||
|
const provider = {
|
||||||
|
prometheusProvider: {
|
||||||
|
type: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
merge(this.props.cluster.preferences, provider);
|
||||||
|
}}
|
||||||
options={options}
|
options={options}
|
||||||
onChange={this.changePrometheusProvider}
|
|
||||||
/>
|
/>
|
||||||
</>;
|
</>
|
||||||
}
|
);
|
||||||
|
|
||||||
@autobind()
|
|
||||||
changePrometheusProvider({ value: prometheusProvider }: SelectProps<string>) {
|
|
||||||
this.prometheusProvider = prometheusProvider;
|
|
||||||
this.props.cluster.preferences.prometheusProvider = { type: prometheusProvider };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,14 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Cluster } from "../../../../main/cluster";
|
|
||||||
import { Input } from "../../input";
|
|
||||||
import { Spinner } from "../../spinner";
|
|
||||||
import { clusterStore } from "../../../../common/cluster-store"
|
|
||||||
import { Icon } from "../../icon";
|
|
||||||
import { Tooltip, TooltipPosition } from "../../tooltip";
|
|
||||||
import { autobind } from "../../../utils";
|
|
||||||
import { TextInputStatus } from "./statuses"
|
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
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 {
|
interface Props {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -17,89 +13,29 @@ interface Props {
|
|||||||
@observer
|
@observer
|
||||||
export class ClusterProxySetting extends React.Component<Props> {
|
export class ClusterProxySetting extends React.Component<Props> {
|
||||||
@observable proxy = this.props.cluster.preferences.httpsProxy || "";
|
@observable proxy = this.props.cluster.preferences.httpsProxy || "";
|
||||||
@observable status = TextInputStatus.CLEAN;
|
|
||||||
@observable errorText?: string;
|
save = () => {
|
||||||
|
this.props.cluster.preferences.httpsProxy = this.proxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange = (value: string) => {
|
||||||
|
this.proxy = value;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <>
|
return (
|
||||||
<h4>HTTPS Proxy</h4>
|
<>
|
||||||
<p>HTTPS Proxy server. Used for communicating with Kubernetes API.</p>
|
<SubTitle title="HTTP Proxy"/>
|
||||||
|
<p>HTTP Proxy server. Used for communicating with Kubernetes API.</p>
|
||||||
<Input
|
<Input
|
||||||
theme="round-black"
|
theme="round-black"
|
||||||
className="box grow"
|
|
||||||
value={this.proxy}
|
value={this.proxy}
|
||||||
onSubmit={this.updateClusterProxy}
|
onChange={this.onChange}
|
||||||
onChange={this.changeProxyState}
|
onBlur={this.save}
|
||||||
iconRight={this.getIconRight()}
|
placeholder="http://<address>:<port>"
|
||||||
placeholder="https://<address>:<port>"
|
validators={isUrl}
|
||||||
/>
|
/>
|
||||||
</>;
|
</>
|
||||||
}
|
);
|
||||||
|
|
||||||
@autobind()
|
|
||||||
changeProxyState(proxy: string, _e: React.ChangeEvent) {
|
|
||||||
if (this.status === TextInputStatus.UPDATING) {
|
|
||||||
console.log("prevent changing cluster proxy while updating");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.status = this.proxyDiffers(proxy);
|
|
||||||
this.proxy = proxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyDiffers(proxy: string): TextInputStatus {
|
|
||||||
const { httpsProxy = "" } = this.props.cluster.preferences;
|
|
||||||
|
|
||||||
return proxy === httpsProxy ? TextInputStatus.CLEAN : TextInputStatus.DIRTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
getIconRight(): React.ReactNode {
|
|
||||||
switch (this.status) {
|
|
||||||
case TextInputStatus.CLEAN:
|
|
||||||
return null;
|
|
||||||
case TextInputStatus.DIRTY:
|
|
||||||
return <Icon size="16px" material="fiber_manual_record"/>;
|
|
||||||
case TextInputStatus.UPDATED:
|
|
||||||
return <Icon size="16px" className="updated" material="done"/>;
|
|
||||||
case TextInputStatus.UPDATING:
|
|
||||||
return <Spinner />;
|
|
||||||
case TextInputStatus.ERROR:
|
|
||||||
return <Icon id="cluster-proxy-setting-error-icon" size="16px" material="error">
|
|
||||||
<Tooltip targetId="cluster-proxy-setting-error-icon" position={TooltipPosition.TOP}>
|
|
||||||
{this.errorText}
|
|
||||||
</Tooltip>
|
|
||||||
</Icon>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
updateClusterProxy(proxy: string) {
|
|
||||||
if (this.proxyDiffers(proxy) !== TextInputStatus.DIRTY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = new URL(proxy);
|
|
||||||
|
|
||||||
if (url.protocol !== "https") {
|
|
||||||
this.status = TextInputStatus.ERROR
|
|
||||||
this.errorText= `Proxy's protocol should be "https"`
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (url.port === "") {
|
|
||||||
this.status = TextInputStatus.ERROR
|
|
||||||
this.errorText= "Proxy should include a port"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.status = TextInputStatus.ERROR
|
|
||||||
this.errorText= "Invalid URL"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.status = TextInputStatus.UPDATING
|
|
||||||
this.props.cluster.preferences.httpsProxy = proxy;
|
|
||||||
this.proxy = proxy;
|
|
||||||
this.status = TextInputStatus.UPDATED
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,12 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Cluster } from "../../../../main/cluster";
|
|
||||||
import { clusterStore } from "../../../../common/cluster-store"
|
|
||||||
import { workspaceStore } from "../../../../common/workspace-store"
|
|
||||||
import { Select, SelectOption } from "../../../components/select";
|
|
||||||
import { GeneralInputStatus } from "./statuses"
|
|
||||||
import { observable } from "mobx";
|
|
||||||
import { autobind } from "../../../utils";
|
|
||||||
import { observer } from "mobx-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 {
|
interface Props {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -14,23 +13,24 @@ interface Props {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterWorkspaceSetting extends React.Component<Props> {
|
export class ClusterWorkspaceSetting extends React.Component<Props> {
|
||||||
@observable workspace = this.props.cluster.workspace;
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <>
|
return (
|
||||||
<h4>Cluster Workspace</h4>
|
<>
|
||||||
<p>Change cluster workspace:</p>
|
<SubTitle title="Cluster Workspace"/>
|
||||||
|
<p>
|
||||||
|
Define cluster{" "}
|
||||||
|
<Link to={workspacesURL()}>
|
||||||
|
workspace
|
||||||
|
</Link>.
|
||||||
|
</p>
|
||||||
<Select
|
<Select
|
||||||
value={workspaceStore.currentWorkspaceId}
|
value={this.props.cluster.workspace}
|
||||||
options={workspaceStore.workspacesList.map(w => ({value: w.id, label: <span>{w.name}</span>}))}
|
onChange={({value}) => this.props.cluster.workspace = value}
|
||||||
onChange={this.changeWorkspace}
|
options={workspaceStore.workspacesList.map(w =>
|
||||||
|
({value: w.id, label: w.name})
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</>;
|
</>
|
||||||
}
|
);
|
||||||
|
|
||||||
@autobind()
|
|
||||||
changeWorkspace({ value: workspace }: SelectOption<string>) {
|
|
||||||
this.workspace = workspace;
|
|
||||||
this.props.cluster.workspace = workspace;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,109 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Cluster } from "../../../../main/cluster";
|
|
||||||
import { Button } from "../../button";
|
|
||||||
import { autobind } from "../../../utils";
|
|
||||||
import { Tooltip, TooltipPosition } from "../../tooltip";
|
|
||||||
import { MetricsFeature } from "../../../../features/metrics";
|
|
||||||
import { Spinner } from "../../spinner";
|
|
||||||
import { Icon } from "../../icon";
|
|
||||||
import { clusterIpc } from "../../../../common/cluster-ipc";
|
|
||||||
import { observable } from "mobx";
|
|
||||||
import { ActionStatus } from "./statuses"
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cluster: Cluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class InstallMetrics extends React.Component<Props> {
|
|
||||||
@observable status = ActionStatus.IDLE;
|
|
||||||
@observable errorText?: string;
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <>
|
|
||||||
<h4>Metrics</h4>
|
|
||||||
<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>
|
|
||||||
<div className="center">
|
|
||||||
{this.getActionButtons()}
|
|
||||||
</div>
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatusIcon(): React.ReactNode {
|
|
||||||
switch (this.status) {
|
|
||||||
case ActionStatus.IDLE:
|
|
||||||
return null;
|
|
||||||
case ActionStatus.PROCESSING:
|
|
||||||
return <Spinner />;
|
|
||||||
case ActionStatus.ERROR:
|
|
||||||
return <Icon size="16px" material="error" title={this.errorText}></Icon>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisabledToolTip(id: string, action: string): React.ReactNode {
|
|
||||||
const { cluster } = this.props;
|
|
||||||
if (cluster.isAdmin) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip targetId={id} position={TooltipPosition.TOP}>
|
|
||||||
{action} only allowed by admins
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getActionButtons(): React.ReactNode[] {
|
|
||||||
const { cluster } = this.props
|
|
||||||
const buttons = [];
|
|
||||||
|
|
||||||
if (cluster.features[MetricsFeature.id]?.canUpgrade) {
|
|
||||||
buttons.push(
|
|
||||||
<Button key="upgrade" id="cluster-feature-metrics-upgrade" disabled={!cluster.isAdmin} primary onClick={this.runAction("upgradeFeature")}>
|
|
||||||
Upgrade {this.getStatusIcon()} {this.getDisabledToolTip("cluster-feature-metrics-upgrade", "Upgrading")}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cluster.features[MetricsFeature.id]?.installed) {
|
|
||||||
buttons.push(
|
|
||||||
<Button key="uninstall" id="cluster-feature-metrics-uninstall" disabled={!cluster.isAdmin} primary onClick={this.runAction("uninstallFeature")}>
|
|
||||||
Uninstall {this.getStatusIcon()} {this.getDisabledToolTip("cluster-feature-metrics-uninstall", "Uninstalling")}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
buttons.push(
|
|
||||||
<Button key="install" id="cluster-feature-metrics-install" disabled={!cluster.isAdmin} primary onClick={this.runAction("installFeature")}>
|
|
||||||
Install {this.getStatusIcon()} {this.getDisabledToolTip("cluster-feature-metrics-install", "Installing")}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buttons;
|
|
||||||
}
|
|
||||||
|
|
||||||
runAction(action: keyof typeof clusterIpc): () => Promise<void> {
|
|
||||||
return async () => {
|
|
||||||
const { cluster } = this.props;
|
|
||||||
console.log(`running ${action} ${MetricsFeature.id} onto ${cluster.preferences.clusterName}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.status = ActionStatus.PROCESSING
|
|
||||||
await clusterIpc[action].invokeFromRenderer(cluster.id, MetricsFeature.id);
|
|
||||||
try {
|
|
||||||
await cluster.refresh();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
this.status = ActionStatus.IDLE
|
|
||||||
} catch (err) {
|
|
||||||
this.status = ActionStatus.ERROR
|
|
||||||
this.errorText = err.toString()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Cluster } from "../../../../main/cluster";
|
|
||||||
import { Button } from "../../button";
|
|
||||||
import { autobind } from "../../../utils";
|
|
||||||
import { Tooltip, TooltipPosition } from "../../tooltip";
|
|
||||||
import { Spinner } from "../../spinner";
|
|
||||||
import { Icon } from "../../icon";
|
|
||||||
import { UserModeFeature } from "../../../../features/user-mode";
|
|
||||||
import { clusterIpc } from "../../../../common/cluster-ipc";
|
|
||||||
import { observable } from "mobx";
|
|
||||||
import { ActionStatus } from "./statuses"
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cluster: Cluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class InstallUserMode extends React.Component<Props> {
|
|
||||||
@observable status = ActionStatus.IDLE;
|
|
||||||
@observable errorText?: string;
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <>
|
|
||||||
<h4>User Mode</h4>
|
|
||||||
<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>
|
|
||||||
<div className="center">
|
|
||||||
{this.getActionButtons()}
|
|
||||||
</div>
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
getStatusIcon(): React.ReactNode {
|
|
||||||
switch (this.status) {
|
|
||||||
case ActionStatus.IDLE:
|
|
||||||
return null;
|
|
||||||
case ActionStatus.PROCESSING:
|
|
||||||
return <Spinner key="spinner" />;
|
|
||||||
case ActionStatus.ERROR:
|
|
||||||
return <Icon key="error" size="16px" material="error" title={this.errorText}></Icon>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisabledToolTip(id: string, action: string): React.ReactNode {
|
|
||||||
const { cluster } = this.props;
|
|
||||||
if (cluster.isAdmin) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Tooltip targetId={id} position={TooltipPosition.TOP}>
|
|
||||||
{action} only allowed by admins
|
|
||||||
</Tooltip>;
|
|
||||||
}
|
|
||||||
|
|
||||||
getActionButtons(): React.ReactNode[] {
|
|
||||||
const { cluster } = this.props
|
|
||||||
const buttons = [];
|
|
||||||
|
|
||||||
if (cluster.features[UserModeFeature.id]?.canUpgrade) {
|
|
||||||
buttons.push(
|
|
||||||
<Button key="upgrade" id="cluster-feature-user-mode-upgrade" disabled={!cluster.isAdmin} primary onClick={this.runAction("upgradeFeature")}>
|
|
||||||
Upgrade {this.getStatusIcon()} {this.getDisabledToolTip("cluster-feature-user-mode-upgrade", "Upgrading")}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cluster.features[UserModeFeature.id]?.installed) {
|
|
||||||
buttons.push(
|
|
||||||
<Button key="uninstall" id="cluster-feature-user-mode-uninstall" disabled={!cluster.isAdmin} primary onClick={this.runAction("uninstallFeature")}>
|
|
||||||
Uninstall {this.getStatusIcon()} {this.getDisabledToolTip("cluster-feature-user-mode-uninstall", "Uninstalling")}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
buttons.push(
|
|
||||||
<Button key="install" id="cluster-feature-user-mode-install" disabled={!cluster.isAdmin} primary onClick={this.runAction("installFeature")}>
|
|
||||||
Install {this.getStatusIcon()} {this.getDisabledToolTip("cluster-feature-user-mode-install", "Installing")}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buttons;
|
|
||||||
}
|
|
||||||
|
|
||||||
runAction(action: keyof typeof clusterIpc): () => Promise<void> {
|
|
||||||
return async () => {
|
|
||||||
const { cluster } = this.props;
|
|
||||||
console.log(`running ${action} ${UserModeFeature.id} onto ${cluster.preferences.clusterName}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.status = ActionStatus.PROCESSING
|
|
||||||
await clusterIpc[action].invokeFromRenderer(cluster.id, UserModeFeature.id);
|
|
||||||
try {
|
|
||||||
await cluster.refresh();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
this.status = ActionStatus.IDLE
|
|
||||||
} catch (err) {
|
|
||||||
this.status = ActionStatus.ERROR
|
|
||||||
this.errorText = err.toString()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Cluster } from "../../../../main/cluster";
|
|
||||||
import { Button } from "../../button";
|
|
||||||
import { autobind } from "../../../utils";
|
|
||||||
import { Spinner } from "../../spinner";
|
|
||||||
import { Icon } from "../../icon";
|
|
||||||
import { ConfirmDialog } from "../../confirm-dialog";
|
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
import { clusterIpc } from "../../../../common/cluster-ipc";
|
import { clusterIpc } from "../../../../common/cluster-ipc";
|
||||||
import { clusterStore } from "../../../../common/cluster-store";
|
import { clusterStore } from "../../../../common/cluster-store";
|
||||||
import { observable } from "mobx";
|
import { Cluster } from "../../../../main/cluster";
|
||||||
import { observer } from "mobx-react";
|
import { autobind } from "../../../utils";
|
||||||
import { RemovalStatus } from "./statuses"
|
import { Button } from "../../button";
|
||||||
|
import { ConfirmDialog } from "../../confirm-dialog";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -18,46 +14,24 @@ interface Props {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class RemoveClusterButton extends React.Component<Props> {
|
export class RemoveClusterButton extends React.Component<Props> {
|
||||||
@observable status = RemovalStatus.PRESENT;
|
|
||||||
@observable errorText?: string;
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="center">
|
|
||||||
<Button accent onClick={this.confirmRemoveCluster}>Remove Cluster {this.getStatusIcon()}</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatusIcon(): React.ReactNode {
|
|
||||||
switch (this.status) {
|
|
||||||
case RemovalStatus.PRESENT:
|
|
||||||
return null;
|
|
||||||
case RemovalStatus.PROCESSING:
|
|
||||||
return <Spinner />;
|
|
||||||
case RemovalStatus.ERROR:
|
|
||||||
return <Icon size="16px" material="error" title={this.errorText}></Icon>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
confirmRemoveCluster() {
|
confirmRemoveCluster() {
|
||||||
const { cluster } = this.props;
|
const { cluster } = this.props;
|
||||||
|
|
||||||
ConfirmDialog.open({
|
ConfirmDialog.open({
|
||||||
message: <p>Are you sure you want to remove <b>{cluster.preferences.clusterName}</b> from Lens?</p>,
|
message: <p>Are you sure you want to remove <b>{cluster.preferences.clusterName}</b> from Lens?</p>,
|
||||||
labelOk: <Trans>Yes</Trans>,
|
labelOk: <Trans>Yes</Trans>,
|
||||||
labelCancel: <Trans>No</Trans>,
|
labelCancel: <Trans>No</Trans>,
|
||||||
ok: async () => {
|
ok: async () => {
|
||||||
try {
|
|
||||||
this.status = RemovalStatus.PROCESSING;
|
|
||||||
await clusterIpc.disconnect.invokeFromRenderer(cluster.id);
|
|
||||||
await clusterStore.removeById(cluster.id);
|
await clusterStore.removeById(cluster.id);
|
||||||
} catch (err) {
|
|
||||||
this.status = RemovalStatus.ERROR;
|
|
||||||
this.errorText = err.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Button accent onClick={this.confirmRemoveCluster} className="button-area">
|
||||||
|
Remove Cluster
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,24 +0,0 @@
|
|||||||
export enum TextInputStatus {
|
|
||||||
CLEAN = "clean",
|
|
||||||
DIRTY = "dirty",
|
|
||||||
UPDATING = "updating",
|
|
||||||
ERROR = "error",
|
|
||||||
UPDATED = "updated",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum GeneralInputStatus {
|
|
||||||
CLEAN = "clean",
|
|
||||||
ERROR = "error",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ActionStatus {
|
|
||||||
IDLE = "idle",
|
|
||||||
PROCESSING = "processing",
|
|
||||||
ERROR = "error"
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum RemovalStatus {
|
|
||||||
PRESENT = "present",
|
|
||||||
PROCESSING = "processing",
|
|
||||||
ERROR = "error",
|
|
||||||
}
|
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { InstallMetrics } from "./components/install-metrics";
|
import { InstallFeature } from "./components/install-feature";
|
||||||
import { InstallUserMode } from "./components/install-user-mode";
|
import { SubTitle } from "../layout/sub-title";
|
||||||
|
import { MetricsFeature } from "../../../features/metrics";
|
||||||
|
import { UserModeFeature } from "../../../features/user-mode";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -11,10 +13,30 @@ export class Features extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
const { cluster } = this.props;
|
const { cluster } = this.props;
|
||||||
|
|
||||||
return <div>
|
return (
|
||||||
|
<div>
|
||||||
<h2>Features</h2>
|
<h2>Features</h2>
|
||||||
<InstallMetrics cluster={cluster}/>
|
<InstallFeature cluster={cluster} feature={MetricsFeature.id}>
|
||||||
<InstallUserMode cluster={cluster}/>
|
<>
|
||||||
</div>;
|
<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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -15,8 +15,6 @@ export class General extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
return <div>
|
return <div>
|
||||||
<h2>General</h2>
|
<h2>General</h2>
|
||||||
<hr/>
|
|
||||||
|
|
||||||
<ClusterNameSetting cluster={this.props.cluster} />
|
<ClusterNameSetting cluster={this.props.cluster} />
|
||||||
<ClusterWorkspaceSetting cluster={this.props.cluster} />
|
<ClusterWorkspaceSetting cluster={this.props.cluster} />
|
||||||
<ClusterIconSetting cluster={this.props.cluster} />
|
<ClusterIconSetting cluster={this.props.cluster} />
|
||||||
|
|||||||
@ -10,9 +10,11 @@ export class Removal extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
const { cluster } = this.props;
|
const { cluster } = this.props;
|
||||||
|
|
||||||
return <div>
|
return (
|
||||||
|
<div>
|
||||||
<h2>Removal</h2>
|
<h2>Removal</h2>
|
||||||
<RemoveClusterButton cluster={cluster} />
|
<RemoveClusterButton cluster={cluster} />
|
||||||
</div>;
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,41 +1,40 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Spinner } from "../spinner";
|
|
||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
|
import { SubTitle } from "../layout/sub-title";
|
||||||
|
import { Table, TableCell, TableRow } from "../table";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Status extends React.Component<Props> {
|
export class Status extends React.Component<Props> {
|
||||||
renderStatusRows(): JSX.Element[] {
|
renderStatusRows() {
|
||||||
const { cluster } = this.props;
|
const { cluster } = this.props;
|
||||||
|
const rows = [
|
||||||
const rows: [string, React.ReactNode][] = [
|
|
||||||
["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"}`],
|
["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"}`],
|
||||||
["Distribution", cluster.distribution],
|
["Distribution", cluster.distribution],
|
||||||
["Kerbel Version", cluster.version],
|
["Kerbel Version", cluster.version],
|
||||||
["API Address", cluster.apiUrl],
|
["API Address", cluster.apiUrl],
|
||||||
|
["Nodes Count", cluster.nodes || "0"]
|
||||||
];
|
];
|
||||||
|
return (
|
||||||
if (cluster.nodes > 0) {
|
<Table scrollable={false}>
|
||||||
rows.push(["Nodes Count", cluster.nodes]);
|
{rows.map(([name, value]) => {
|
||||||
}
|
return (
|
||||||
|
<TableRow key={name}>
|
||||||
return rows
|
<TableCell>{name}</TableCell>
|
||||||
.map(([header, value]) => [
|
<TableCell className="value">{value}</TableCell>
|
||||||
<h5 key={header+"-header"}>{header}</h5>,
|
</TableRow>
|
||||||
<span key={header + "-value"}>{value}</span>
|
);
|
||||||
])
|
})}
|
||||||
.flat();
|
</Table>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { cluster } = this.props;
|
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<h2>Status</h2>
|
<h2>Status</h2>
|
||||||
<hr/>
|
<SubTitle title="Cluster Status"/>
|
||||||
<h4>Cluster status</h4>
|
|
||||||
<p>
|
<p>
|
||||||
Cluster status information including: detected distribution, kernel version, and online status.
|
Cluster status information including: detected distribution, kernel version, and online status.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -7,6 +7,12 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.interactive {
|
||||||
|
img {
|
||||||
|
opacity: .55;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.active, &.interactive:hover {
|
&.active, &.interactive:hover {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
|
||||||
@ -16,7 +22,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
opacity: .55;
|
|
||||||
width: var(--size);
|
width: var(--size);
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,12 +42,12 @@ export class ClusterIcon extends React.Component<Props> {
|
|||||||
active: isActive,
|
active: isActive,
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div {...elemProps} className={className} id={clusterIconId}>
|
<div {...elemProps} className={className} id={showTooltip ? clusterIconId : null}>
|
||||||
{showTooltip && (
|
{showTooltip && (
|
||||||
<Tooltip targetId={clusterIconId}>{clusterName}</Tooltip>
|
<Tooltip targetId={clusterIconId}>{clusterName}</Tooltip>
|
||||||
)}
|
)}
|
||||||
{icon && <img src={icon} alt={clusterName}/>}
|
{icon && <img src={icon} alt={clusterName}/>}
|
||||||
{!icon && <Hashicon value={clusterName} options={options}/>}
|
{!icon && <Hashicon value={clusterId} options={options}/>}
|
||||||
{showErrors && isAdmin && eventCount > 0 && (
|
{showErrors && isAdmin && eventCount > 0 && (
|
||||||
<Badge
|
<Badge
|
||||||
className={cssNames("events-count", errorClass)}
|
className={cssNames("events-count", errorClass)}
|
||||||
|
|||||||
@ -33,11 +33,8 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
@observable showHint = true;
|
@observable showHint = true;
|
||||||
|
|
||||||
showCluster = (clusterId: ClusterId) => {
|
showCluster = (clusterId: ClusterId) => {
|
||||||
if (clusterStore.activeClusterId === clusterId) {
|
clusterStore.setActive(clusterId);
|
||||||
navigate("/"); // redirect to index
|
navigate("/"); // redirect to index
|
||||||
} else {
|
|
||||||
clusterStore.activeClusterId = clusterId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addCluster = () => {
|
addCluster = () => {
|
||||||
@ -50,7 +47,10 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
|
|
||||||
menu.append(new MenuItem({
|
menu.append(new MenuItem({
|
||||||
label: _i18n._(t`Settings`),
|
label: _i18n._(t`Settings`),
|
||||||
click: () => navigate(clusterSettingsURL())
|
click: () => {
|
||||||
|
clusterStore.setActive(cluster.id);
|
||||||
|
navigate(clusterSettingsURL())
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
if (cluster.online) {
|
if (cluster.online) {
|
||||||
menu.append(new MenuItem({
|
menu.append(new MenuItem({
|
||||||
|
|||||||
@ -4,11 +4,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
border: medium solid;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 5px;
|
color: var(--blue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,7 +43,7 @@ export enum OverTotalSizeLimitStyle {
|
|||||||
|
|
||||||
export interface BaseProps {
|
export interface BaseProps {
|
||||||
accept?: string;
|
accept?: string;
|
||||||
labelText: string;
|
label: React.ReactNode;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
|
||||||
// limit is the optional maximum number of files to upload
|
// limit is the optional maximum number of files to upload
|
||||||
@ -175,10 +175,10 @@ export class FilePicker extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { accept, labelText, multiple } = this.props;
|
const { accept, label, multiple } = this.props;
|
||||||
|
|
||||||
return <div className="FilePicker">
|
return <div className="FilePicker">
|
||||||
<label htmlFor="file-upload">{labelText} {this.getIconRight()}</label>
|
<label className="flex gaps align-center" htmlFor="file-upload">{label} {this.getIconRight()}</label>
|
||||||
<input
|
<input
|
||||||
id="file-upload"
|
id="file-upload"
|
||||||
name="FilePicker"
|
name="FilePicker"
|
||||||
|
|||||||
@ -74,7 +74,7 @@
|
|||||||
|
|
||||||
.input-info {
|
.input-info {
|
||||||
.errors {
|
.errors {
|
||||||
color: var(color-error);
|
color: var(--colorError);
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -285,6 +285,7 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
rows: multiLine ? (rows || 1) : null,
|
rows: multiLine ? (rows || 1) : null,
|
||||||
ref: this.bindRef,
|
ref: this.bindRef,
|
||||||
type: "text",
|
type: "text",
|
||||||
|
spellCheck: "false",
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -13,6 +13,11 @@
|
|||||||
padding: $spacing;
|
padding: $spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .head-col {
|
||||||
|
position: sticky;
|
||||||
|
border-bottom: 1px solid $grey-800;
|
||||||
|
}
|
||||||
|
|
||||||
> .content-col {
|
> .content-col {
|
||||||
margin-right: $spacing;
|
margin-right: $spacing;
|
||||||
background-color: var(--clusters-menu-bgc);
|
background-color: var(--clusters-menu-bgc);
|
||||||
@ -29,6 +34,10 @@
|
|||||||
border-left: 1px solid #353a3e;
|
border-left: 1px solid #353a3e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 140%;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $colorInfo;
|
color: $colorInfo;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { cssNames, IClassName } from "../../utils";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: IClassName;
|
className?: IClassName;
|
||||||
|
header?: React.ReactNode;
|
||||||
|
headerClass?: IClassName;
|
||||||
contentClass?: IClassName;
|
contentClass?: IClassName;
|
||||||
infoPanelClass?: IClassName;
|
infoPanelClass?: IClassName;
|
||||||
infoPanel?: React.ReactNode;
|
infoPanel?: React.ReactNode;
|
||||||
@ -13,9 +15,14 @@ interface Props {
|
|||||||
@observer
|
@observer
|
||||||
export class WizardLayout extends React.Component<Props> {
|
export class WizardLayout extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { className, contentClass, infoPanelClass, infoPanel, children: content } = this.props;
|
const { className, contentClass, infoPanelClass, infoPanel, header, headerClass, children: content } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("WizardLayout", className)}>
|
<div className={cssNames("WizardLayout", className)}>
|
||||||
|
{header && (
|
||||||
|
<div className={cssNames("head-col flex gaps align-center", headerClass)}>
|
||||||
|
{header}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className={cssNames("content-col flex column gaps", contentClass)}>
|
<div className={cssNames("content-col flex column gaps", contentClass)}>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -31,7 +31,7 @@ html {
|
|||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
box-shadow: 0 0 0 1px $halfGray;
|
box-shadow: 0 0 0 1px $borderFaintColor;
|
||||||
|
|
||||||
&--is-focused {
|
&--is-focused {
|
||||||
box-shadow: 0 0 0 2px $primary;
|
box-shadow: 0 0 0 2px $primary;
|
||||||
@ -42,6 +42,10 @@ html {
|
|||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__single-value {
|
||||||
|
color: var(--textColorSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
&__indicator {
|
&__indicator {
|
||||||
padding: $padding /2;
|
padding: $padding /2;
|
||||||
opacity: .55;
|
opacity: .55;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user