1
0
mirror of https://github.com/lensapp/lens.git synced 2024-11-13 09:17:30 +03:00

Add mechanism for users to specify accessible namespaces (#702)

* Add mechanism for users to specify namespaces that are accessible to them. This is generally useful for when the user doesn't have permission to list the namespaces.

- Add new component "EditableList" which provides a simple way to
  display a list of items that can be added too.

- Add the ClusterAccessibleNamespaces to the GeneralClusterSettings

- style editable list list of lists, switch to using <>

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

Co-authored-by: Sebastian Malton <smalton@mirantis.com>
This commit is contained in:
Sebastian Malton 2020-11-09 10:45:09 -05:00 committed by GitHub
parent 16fb35e3f9
commit d0ada00a18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1969 additions and 1793 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -39,6 +39,7 @@ export interface ClusterModel {
preferences?: ClusterPreferences;
metadata?: ClusterMetadata;
ownerRef?: string;
accessibleNamespaces?: string[];
/** @deprecated */
kubeConfig?: string; // yaml
@ -179,8 +180,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
@action
addCluster(model: ClusterModel | Cluster ): Cluster {
appEventBus.emit({name: "cluster", action: "add"})
addCluster(model: ClusterModel | Cluster): Cluster {
appEventBus.emit({ name: "cluster", action: "add" })
let cluster = model as Cluster;
if (!(model instanceof Cluster)) {
cluster = new Cluster(model)
@ -195,7 +196,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action
async removeById(clusterId: ClusterId) {
appEventBus.emit({name: "cluster", action: "remove"})
appEventBus.emit({ name: "cluster", action: "remove" })
const cluster = this.getById(clusterId);
if (cluster) {
this.clusters.delete(clusterId);

View File

@ -80,13 +80,14 @@ export class Cluster implements ClusterModel, ClusterState {
@observable metadata: ClusterMetadata = {};
@observable allowedNamespaces: string[] = [];
@observable allowedResources: string[] = [];
@observable accessibleNamespaces: string[] = [];
@computed get available() {
return this.accessible && !this.disconnected;
}
get version(): string {
return String(this.metadata?.version) || ""
return String(this.metadata?.version) || ""
}
constructor(model: ClusterModel) {
@ -149,7 +150,7 @@ export class Cluster implements ClusterModel, ClusterState {
}
@action
async activate(force = false ) {
async activate(force = false) {
if (this.activated && !force) {
return this.pushState();
}
@ -340,7 +341,7 @@ export class Cluster implements ClusterModel, ClusterState {
for (const w of warnings) {
if (w.involvedObject.kind === 'Pod') {
try {
const pod = (await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace)).body;
const { body: pod } = await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace);
logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`)
if (podHasIssues(pod)) {
uniqEventSources.add(w.involvedObject.uid);
@ -351,11 +352,10 @@ export class Cluster implements ClusterModel, ClusterState {
uniqEventSources.add(w.involvedObject.uid);
}
}
let nodeNotificationCount = 0;
const nodes = (await client.listNode()).body.items;
nodes.map(n => {
nodeNotificationCount = nodeNotificationCount + getNodeWarningConditions(n).length
});
const nodeNotificationCount = nodes
.map(getNodeWarningConditions)
.reduce((sum, conditions) => sum + conditions.length, 0);
return uniqEventSources.size + nodeNotificationCount;
} catch (error) {
logger.error("Failed to fetch event count: " + JSON.stringify(error))
@ -371,7 +371,8 @@ export class Cluster implements ClusterModel, ClusterState {
workspace: this.workspace,
preferences: this.preferences,
metadata: this.metadata,
ownerRef: this.ownerRef
ownerRef: this.ownerRef,
accessibleNamespaces: this.accessibleNamespaces,
};
return toJS(model, {
recurseEverything: true
@ -426,6 +427,10 @@ export class Cluster implements ClusterModel, ClusterState {
}
protected async getAllowedNamespaces() {
if (this.accessibleNamespaces.length) {
return this.accessibleNamespaces
}
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api)
try {
const namespaceList = await api.listNamespace()
@ -442,7 +447,7 @@ export class Cluster implements ClusterModel, ClusterState {
} catch (error) {
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName)
if (ctx.namespace) return [ctx.namespace]
return []
return [];
}
}

View File

@ -0,0 +1,38 @@
import React from "react";
import { observer } from "mobx-react";
import { Cluster } from "../../../../main/cluster";
import { SubTitle } from "../../layout/sub-title";
import { EditableList } from "../../editable-list";
import { observable } from "mobx";
import { _i18n } from "../../../i18n";
import { Trans } from "@lingui/macro";
interface Props {
cluster: Cluster;
}
@observer
export class ClusterAccessibleNamespaces extends React.Component<Props> {
@observable namespaces = new Set(this.props.cluster.accessibleNamespaces);
render() {
return (
<>
<SubTitle title="Accessible Namespaces" />
<p><Trans>This setting is useful for manually specifying which namespaces you have access to. This is useful when you don't have permissions to list namespaces.</Trans></p>
<EditableList
placeholder={_i18n._("Add new namespace...")}
add={(newNamespace) => {
this.namespaces.add(newNamespace);
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
}}
items={Array.from(this.namespaces)}
remove={({ oldItem: oldNamesapce }) => {
this.namespaces.delete(oldNamesapce);
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
}}
/>
</>
);
}
}

View File

@ -6,6 +6,7 @@ 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";
import { ClusterAccessibleNamespaces } from "./components/cluster-accessible-namespaces";
interface Props {
cluster: Cluster;
@ -21,6 +22,7 @@ export class General extends React.Component<Props> {
<ClusterProxySetting cluster={this.props.cluster} />
<ClusterPrometheusSetting cluster={this.props.cluster} />
<ClusterHomeDirSetting cluster={this.props.cluster} />
<ClusterAccessibleNamespaces cluster={this.props.cluster} />
</div>;
}
}

View File

@ -0,0 +1,28 @@
.EditableList {
.el-contents {
display: flex;
flex-direction: column;
margin-top: $padding * 2;
.el-value-remove {
.Icon {
justify-content: unset;
}
}
.el-item {
display: grid;
grid-template-columns: 1fr auto;
padding: $padding $padding * 2;
margin-bottom: 1px;
:last-child {
margin-bottom: unset;
}
:first-child {
align-self: center;
}
}
}
}

View File

@ -0,0 +1,71 @@
import "./editable-list.scss"
import React from "react";
import { Icon } from "../icon";
import { Input } from "../input";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { autobind } from "../../utils";
import { _i18n } from "../../i18n";
export interface Props<T> {
items: T[],
add: (newItem: string) => void,
remove: (info: { oldItem: T, index: number }) => void,
placeholder?: string,
// An optional prop used to convert T to a displayable string
// defaults to `String`
renderItem?: (item: T, index: number) => React.ReactNode,
}
const defaultProps: Partial<Props<any>> = {
placeholder: _i18n._("Add new item..."),
renderItem: (item: any, index: number) => <React.Fragment key={index}>{item}</React.Fragment>
}
@observer
export class EditableList<T> extends React.Component<Props<T>> {
static defaultProps = defaultProps as Props<any>;
@observable currentNewItem = "";
@autobind()
onSubmit(val: string) {
const { add } = this.props
if (val) {
add(val)
this.currentNewItem = ""
}
}
render() {
const { items, remove, renderItem, placeholder } = this.props;
return (
<div className="EditableList">
<div className="el-header">
<Input
theme="round-black"
value={this.currentNewItem}
onSubmit={this.onSubmit}
placeholder={placeholder}
onChange={val => this.currentNewItem = val}
/>
</div>
<div className="el-contents">
{
items.map((item, index) => (
<div key={item + `${index}`} className="el-item Badge">
<div>{renderItem(item, index)}</div>
<div className="el-value-remove">
<Icon material="delete_outline" onClick={() => remove(({ index, oldItem: item }))} />
</div>
</div>
))
}
</div>
</div>
)
}
}

View File

@ -0,0 +1 @@
export * from "./editable-list"