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:
parent
16fb35e3f9
commit
d0ada00a18
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
@ -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);
|
||||
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>;
|
||||
}
|
||||
}
|
28
src/renderer/components/editable-list/editable-list.scss
Normal file
28
src/renderer/components/editable-list/editable-list.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
71
src/renderer/components/editable-list/editable-list.tsx
Normal file
71
src/renderer/components/editable-list/editable-list.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
1
src/renderer/components/editable-list/index.ts
Normal file
1
src/renderer/components/editable-list/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./editable-list"
|
Loading…
Reference in New Issue
Block a user