mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Several fixes:
- Not being able to type in the ClusterRole add dialog - Editing not working as intended on subjects for CRBs and RBs Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
63de3dc76b
commit
7c66cd2db0
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ObservableHashSet } from "../hash-set";
|
import { HashSet, ObservableHashSet } from "../hash-set";
|
||||||
|
|
||||||
describe("ObservableHashSet<T>", () => {
|
describe("ObservableHashSet<T>", () => {
|
||||||
it("should not throw on creation", () => {
|
it("should not throw on creation", () => {
|
||||||
@ -273,3 +273,256 @@ describe("ObservableHashSet<T>", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("HashSet<T>", () => {
|
||||||
|
it("should not throw on creation", () => {
|
||||||
|
expect(() => new HashSet<{ a: number }>([], item => item.a.toString())).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be initializable", () => {
|
||||||
|
const res = new HashSet([
|
||||||
|
{ a: 1 },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
], item => item.a.toString());
|
||||||
|
|
||||||
|
expect(res.size).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has should work as expected", () => {
|
||||||
|
const res = new HashSet([
|
||||||
|
{ a: 1 },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
], item => item.a.toString());
|
||||||
|
|
||||||
|
expect(res.has({ a: 1 })).toBe(true);
|
||||||
|
expect(res.has({ a: 5 })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forEach should work as expected", () => {
|
||||||
|
const res = new HashSet([
|
||||||
|
{ a: 1 },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
], item => item.a.toString());
|
||||||
|
|
||||||
|
let a = 1;
|
||||||
|
|
||||||
|
res.forEach((item) => {
|
||||||
|
expect(item.a).toEqual(a++);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delete should work as expected", () => {
|
||||||
|
const res = new HashSet([
|
||||||
|
{ a: 1 },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
], item => item.a.toString());
|
||||||
|
|
||||||
|
expect(res.has({ a: 1 })).toBe(true);
|
||||||
|
expect(res.delete({ a: 1 })).toBe(true);
|
||||||
|
expect(res.has({ a: 1 })).toBe(false);
|
||||||
|
|
||||||
|
expect(res.has({ a: 5 })).toBe(false);
|
||||||
|
expect(res.delete({ a: 5 })).toBe(false);
|
||||||
|
expect(res.has({ a: 5 })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggle should work as expected", () => {
|
||||||
|
const res = new HashSet([
|
||||||
|
{ a: 1 },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
], item => item.a.toString());
|
||||||
|
|
||||||
|
expect(res.has({ a: 1 })).toBe(true);
|
||||||
|
res.toggle({ a: 1 });
|
||||||
|
expect(res.has({ a: 1 })).toBe(false);
|
||||||
|
|
||||||
|
expect(res.has({ a: 6 })).toBe(false);
|
||||||
|
res.toggle({ a: 6 });
|
||||||
|
expect(res.has({ a: 6 })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("add should work as expected", () => {
|
||||||
|
const res = new HashSet([
|
||||||
|
{ a: 1 },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
], item => item.a.toString());
|
||||||
|
|
||||||
|
expect(res.has({ a: 6 })).toBe(false);
|
||||||
|
res.add({ a: 6 });
|
||||||
|
expect(res.has({ a: 6 })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("add should treat the hash to be the same as equality", () => {
|
||||||
|
const res = new HashSet([
|
||||||
|
{ a: 1, foobar: "hello" },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
], item => item.a.toString());
|
||||||
|
|
||||||
|
expect(res.has({ a: 1 })).toBe(true);
|
||||||
|
res.add({ a: 1, foobar: "goodbye" });
|
||||||
|
expect(res.has({ a: 1 })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clear should work as expected", () => {
|
||||||
|
const res = new HashSet([
|
||||||
|
{ a: 1 },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
], item => item.a.toString());
|
||||||
|
|
||||||
|
expect(res.size).toBe(4);
|
||||||
|
res.clear();
|
||||||
|
expect(res.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replace should work as expected", () => {
|
||||||
|
const res = new HashSet([
|
||||||
|
{ a: 1 },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
], item => item.a.toString());
|
||||||
|
|
||||||
|
expect(res.size).toBe(4);
|
||||||
|
res.replace([{ a: 13 }]);
|
||||||
|
expect(res.size).toBe(1);
|
||||||
|
expect(res.has({ a: 1 })).toBe(false);
|
||||||
|
expect(res.has({ a: 13 })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toJSON should work as expected", () => {
|
||||||
|
const res = new HashSet([
|
||||||
|
{ a: 1 },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
], item => item.a.toString());
|
||||||
|
|
||||||
|
expect(res.toJSON()).toStrictEqual([
|
||||||
|
{ a: 1 },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("values should work as expected", () => {
|
||||||
|
const res = new HashSet([
|
||||||
|
{ a: 1 },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
], item => item.a.toString());
|
||||||
|
const iter = res.values();
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: { a: 1 },
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: { a: 2 },
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: { a: 3 },
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: { a: 4 },
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: undefined,
|
||||||
|
done: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keys should work as expected", () => {
|
||||||
|
const res = new HashSet([
|
||||||
|
{ a: 1 },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
], item => item.a.toString());
|
||||||
|
const iter = res.keys();
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: { a: 1 },
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: { a: 2 },
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: { a: 3 },
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: { a: 4 },
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: undefined,
|
||||||
|
done: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("entries should work as expected", () => {
|
||||||
|
const res = new HashSet([
|
||||||
|
{ a: 1 },
|
||||||
|
{ a: 2 },
|
||||||
|
{ a: 3 },
|
||||||
|
{ a: 4 },
|
||||||
|
], item => item.a.toString());
|
||||||
|
const iter = res.entries();
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: [{ a: 1 }, { a: 1 }],
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: [{ a: 2 }, { a: 2 }],
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: [{ a: 3 }, { a: 3 }],
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: [{ a: 4 }, { a: 4 }],
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(iter.next()).toStrictEqual({
|
||||||
|
value: undefined,
|
||||||
|
done: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -27,6 +27,117 @@ export function makeIterableIterator<T>(iterator: Iterator<T>): IterableIterator
|
|||||||
return iterator as IterableIterator<T>;
|
return iterator as IterableIterator<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class HashSet<T> implements Set<T> {
|
||||||
|
#hashmap: Map<string, T>;
|
||||||
|
|
||||||
|
constructor(initialValues: Iterable<T>, protected hasher: (item: T) => string) {
|
||||||
|
this.#hashmap = new Map<string, T>(Array.from(initialValues, value => [this.hasher(value), value]));
|
||||||
|
}
|
||||||
|
|
||||||
|
replace(other: ObservableHashSet<T> | ObservableSet<T> | Set<T> | readonly T[]): this {
|
||||||
|
if (other === null || other === undefined) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(Array.isArray(other) || other instanceof Set || other instanceof ObservableHashSet || other instanceof ObservableSet)) {
|
||||||
|
throw new Error(`ObservableHashSet: Cannot initialize set from ${other}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
for (const value of other) {
|
||||||
|
this.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.#hashmap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
add(value: T): this {
|
||||||
|
this.#hashmap.set(this.hasher(value), value);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(value: T): void {
|
||||||
|
const hash = this.hasher(value);
|
||||||
|
|
||||||
|
if (this.#hashmap.has(hash)) {
|
||||||
|
this.#hashmap.delete(hash);
|
||||||
|
} else {
|
||||||
|
this.#hashmap.set(hash, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(value: T): boolean {
|
||||||
|
return this.#hashmap.delete(this.hasher(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(callbackfn: (value: T, key: T, set: Set<T>) => void, thisArg?: any): void {
|
||||||
|
this.#hashmap.forEach(value => callbackfn(value, value, thisArg ?? this));
|
||||||
|
}
|
||||||
|
|
||||||
|
has(value: T): boolean {
|
||||||
|
return this.#hashmap.has(this.hasher(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
get size(): number {
|
||||||
|
return this.#hashmap.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries(): IterableIterator<[T, T]> {
|
||||||
|
let nextIndex = 0;
|
||||||
|
const keys = Array.from(this.keys());
|
||||||
|
const values = Array.from(this.values());
|
||||||
|
|
||||||
|
return makeIterableIterator<[T, T]>({
|
||||||
|
next() {
|
||||||
|
const index = nextIndex++;
|
||||||
|
|
||||||
|
return index < values.length
|
||||||
|
? { value: [keys[index], values[index]], done: false }
|
||||||
|
: { done: true, value: undefined };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
keys(): IterableIterator<T> {
|
||||||
|
return this.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
values(): IterableIterator<T> {
|
||||||
|
let nextIndex = 0;
|
||||||
|
const observableValues = Array.from(this.#hashmap.values());
|
||||||
|
|
||||||
|
return makeIterableIterator<T>({
|
||||||
|
next: () => {
|
||||||
|
return nextIndex < observableValues.length
|
||||||
|
? { value: observableValues[nextIndex++], done: false }
|
||||||
|
: { done: true, value: undefined };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator](): IterableIterator<T> {
|
||||||
|
return this.#hashmap.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
get [Symbol.toStringTag](): string {
|
||||||
|
return "Set";
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): T[] {
|
||||||
|
return Array.from(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return "[object Set]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ObservableHashSet<T> implements Set<T>, IInterceptable<ISetWillChange>, IListenable {
|
export class ObservableHashSet<T> implements Set<T>, IInterceptable<ISetWillChange>, IListenable {
|
||||||
#hashmap: ObservableMap<string, T>;
|
#hashmap: ObservableMap<string, T>;
|
||||||
|
|
||||||
@ -44,16 +155,20 @@ export class ObservableHashSet<T> implements Set<T>, IInterceptable<ISetWillChan
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
replace(other: ObservableHashSet<T> | ObservableSet<T> | Set<T> | readonly T[]): this {
|
replace(other: ObservableHashSet<T> | ObservableSet<T> | Set<T> | readonly T[]): this {
|
||||||
if (Array.isArray(other)) {
|
if (other === null || other === undefined) {
|
||||||
this.clear();
|
return this;
|
||||||
other.forEach(value => this.add(value));
|
}
|
||||||
} else if (other instanceof Set || other instanceof ObservableHashSet || other instanceof ObservableSet) {
|
|
||||||
this.clear();
|
if (!(Array.isArray(other) || other instanceof Set || other instanceof ObservableHashSet || other instanceof ObservableSet)) {
|
||||||
other.forEach(value => this.add(value));
|
|
||||||
} else if (other !== null && other !== undefined) {
|
|
||||||
throw new Error(`ObservableHashSet: Cannot initialize set from ${other}`);
|
throw new Error(`ObservableHashSet: Cannot initialize set from ${other}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
for (const value of other) {
|
||||||
|
this.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,14 +21,14 @@
|
|||||||
|
|
||||||
import "./details.scss";
|
import "./details.scss";
|
||||||
|
|
||||||
import { observable, reaction } from "mobx";
|
import { reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { KubeEventDetails } from "../../+events/kube-event-details";
|
import { KubeEventDetails } from "../../+events/kube-event-details";
|
||||||
import type { ClusterRoleBinding, ClusterRoleBindingSubject } from "../../../api/endpoints";
|
import type { ClusterRoleBinding, ClusterRoleBindingSubject } from "../../../api/endpoints";
|
||||||
import { kubeObjectDetailRegistry } from "../../../api/kube-object-detail-registry";
|
import { kubeObjectDetailRegistry } from "../../../api/kube-object-detail-registry";
|
||||||
import { autoBind, prevDefault } from "../../../utils";
|
import { autoBind, ObservableHashSet, prevDefault } from "../../../utils";
|
||||||
import { AddRemoveButtons } from "../../add-remove-buttons";
|
import { AddRemoveButtons } from "../../add-remove-buttons";
|
||||||
import { ConfirmDialog } from "../../confirm-dialog";
|
import { ConfirmDialog } from "../../confirm-dialog";
|
||||||
import { DrawerTitle } from "../../drawer";
|
import { DrawerTitle } from "../../drawer";
|
||||||
@ -37,13 +37,14 @@ import { KubeObjectMeta } from "../../kube-object/kube-object-meta";
|
|||||||
import { Table, TableCell, TableHead, TableRow } from "../../table";
|
import { Table, TableCell, TableHead, TableRow } from "../../table";
|
||||||
import { ClusterRoleBindingDialog } from "./dialog";
|
import { ClusterRoleBindingDialog } from "./dialog";
|
||||||
import { clusterRoleBindingsStore } from "./store";
|
import { clusterRoleBindingsStore } from "./store";
|
||||||
|
import { hashClusterRoleBindingSubject } from "./hashers";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<ClusterRoleBinding> {
|
interface Props extends KubeObjectDetailsProps<ClusterRoleBinding> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterRoleBindingDetails extends React.Component<Props> {
|
export class ClusterRoleBindingDetails extends React.Component<Props> {
|
||||||
@observable selectedSubjects = observable.array<ClusterRoleBindingSubject>([], { deep: false });
|
selectedSubjects = new ObservableHashSet<ClusterRoleBindingSubject>([], hashClusterRoleBindingSubject);
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -58,23 +59,12 @@ export class ClusterRoleBindingDetails extends React.Component<Props> {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectSubject(subject: ClusterRoleBindingSubject) {
|
|
||||||
const { selectedSubjects } = this;
|
|
||||||
const isSelected = selectedSubjects.includes(subject);
|
|
||||||
|
|
||||||
selectedSubjects.replace(
|
|
||||||
isSelected
|
|
||||||
? selectedSubjects.filter(sub => sub !== subject) // unselect
|
|
||||||
: selectedSubjects.concat(subject) // select
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeSelectedSubjects() {
|
removeSelectedSubjects() {
|
||||||
const { object: clusterRoleBinding } = this.props;
|
const { object: clusterRoleBinding } = this.props;
|
||||||
const { selectedSubjects } = this;
|
const { selectedSubjects } = this;
|
||||||
|
|
||||||
ConfirmDialog.open({
|
ConfirmDialog.open({
|
||||||
ok: () => clusterRoleBindingsStore.updateSubjects({ clusterRoleBinding, removeSubjects: selectedSubjects }),
|
ok: () => clusterRoleBindingsStore.removeSubjects(clusterRoleBinding, selectedSubjects),
|
||||||
labelOk: `Remove`,
|
labelOk: `Remove`,
|
||||||
message: (
|
message: (
|
||||||
<p>Remove selected bindings for <b>{clusterRoleBinding.getName()}</b>?</p>
|
<p>Remove selected bindings for <b>{clusterRoleBinding.getName()}</b>?</p>
|
||||||
@ -115,19 +105,19 @@ export class ClusterRoleBindingDetails extends React.Component<Props> {
|
|||||||
<Table selectable className="bindings box grow">
|
<Table selectable className="bindings box grow">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableCell checkbox />
|
<TableCell checkbox />
|
||||||
<TableCell className="binding">Binding</TableCell>
|
<TableCell className="binding">Name</TableCell>
|
||||||
<TableCell className="type">Type</TableCell>
|
<TableCell className="type">Type</TableCell>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{
|
{
|
||||||
subjects.map((subject, i) => {
|
subjects.map((subject, i) => {
|
||||||
const { kind, name } = subject;
|
const { kind, name } = subject;
|
||||||
const isSelected = selectedSubjects.includes(subject);
|
const isSelected = selectedSubjects.has(subject);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={i}
|
key={i}
|
||||||
selected={isSelected}
|
selected={isSelected}
|
||||||
onClick={prevDefault(() => this.selectSubject(subject))}
|
onClick={prevDefault(() => this.selectedSubjects.toggle(subject))}
|
||||||
>
|
>
|
||||||
<TableCell checkbox isChecked={isSelected} />
|
<TableCell checkbox isChecked={isSelected} />
|
||||||
<TableCell className="binding">{name}</TableCell>
|
<TableCell className="binding">{name}</TableCell>
|
||||||
@ -141,7 +131,7 @@ export class ClusterRoleBindingDetails extends React.Component<Props> {
|
|||||||
|
|
||||||
<AddRemoveButtons
|
<AddRemoveButtons
|
||||||
onAdd={() => ClusterRoleBindingDialog.open(clusterRoleBinding)}
|
onAdd={() => ClusterRoleBindingDialog.open(clusterRoleBinding)}
|
||||||
onRemove={selectedSubjects.length ? this.removeSelectedSubjects : null}
|
onRemove={selectedSubjects.size ? this.removeSelectedSubjects : null}
|
||||||
addTooltip={`Add bindings to ${roleRef.name}`}
|
addTooltip={`Add bindings to ${roleRef.name}`}
|
||||||
removeTooltip={`Remove selected bindings from ${roleRef.name}`}
|
removeTooltip={`Remove selected bindings from ${roleRef.name}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -170,10 +170,7 @@ export class ClusterRoleBindingDialog extends React.Component<Props> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { selfLink } = this.isEditing
|
const { selfLink } = this.isEditing
|
||||||
? await clusterRoleBindingsStore.updateSubjects({
|
? await clusterRoleBindingsStore.updateSubjects(this.clusterRoleBinding, selectedBindings)
|
||||||
clusterRoleBinding: this.clusterRoleBinding,
|
|
||||||
addSubjects: selectedBindings,
|
|
||||||
})
|
|
||||||
: await clusterRoleBindingsStore.create({ name: bindingName }, {
|
: await clusterRoleBindingsStore.create({ name: bindingName }, {
|
||||||
subjects: selectedBindings,
|
subjects: selectedBindings,
|
||||||
roleRef: {
|
roleRef: {
|
||||||
@ -238,16 +235,11 @@ export class ClusterRoleBindingDialog extends React.Component<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { ...dialogProps } = this.props;
|
const { ...dialogProps } = this.props;
|
||||||
const { isEditing, clusterRoleBinding: roleBinding, selectedRoleRef, selectedBindings } = this;
|
const { isEditing, clusterRoleBinding, selectedRoleRef, selectedBindings } = this;
|
||||||
const roleBindingName = roleBinding ? roleBinding.getName() : "";
|
const clusterRoleBindingName = clusterRoleBinding?.getName();
|
||||||
const header = (
|
const header = clusterRoleBindingName
|
||||||
<h5>
|
? <h5>Edit ClusterRoleBinding <span className="name">{clusterRoleBindingName}</span></h5>
|
||||||
{roleBindingName
|
: <h5>Add ClusterRoleBinding</h5>;
|
||||||
? <>Edit ClusterRoleBinding <span className="name">{roleBindingName}</span></>
|
|
||||||
: "Add ClusterRoleBinding"
|
|
||||||
}
|
|
||||||
</h5>
|
|
||||||
);
|
|
||||||
const disableNext = !selectedRoleRef || !selectedBindings.length;
|
const disableNext = !selectedRoleRef || !selectedBindings.length;
|
||||||
const nextLabel = isEditing ? "Update" : "Create";
|
const nextLabel = isEditing ? "Update" : "Create";
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MD5 } from "crypto-js";
|
||||||
|
import type { ClusterRoleBindingSubject } from "../../../api/endpoints";
|
||||||
|
|
||||||
|
export function hashClusterRoleBindingSubject(subject: ClusterRoleBindingSubject): string {
|
||||||
|
return MD5(JSON.stringify([
|
||||||
|
["kind", subject.kind],
|
||||||
|
["name", subject.name],
|
||||||
|
["apiGroup", subject.apiGroup],
|
||||||
|
])).toString();
|
||||||
|
}
|
||||||
@ -18,13 +18,12 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import difference from "lodash/difference";
|
|
||||||
import uniqBy from "lodash/uniqBy";
|
|
||||||
|
|
||||||
import { apiManager } from "../../../api/api-manager";
|
import { apiManager } from "../../../api/api-manager";
|
||||||
import { ClusterRoleBinding, clusterRoleBindingApi, ClusterRoleBindingSubject } from "../../../api/endpoints";
|
import { ClusterRoleBinding, clusterRoleBindingApi, ClusterRoleBindingSubject } from "../../../api/endpoints";
|
||||||
import { KubeObjectStore } from "../../../kube-object.store";
|
import { KubeObjectStore } from "../../../kube-object.store";
|
||||||
import { autoBind } from "../../../utils";
|
import { autoBind, HashSet } from "../../../utils";
|
||||||
|
import { hashClusterRoleBindingSubject } from "./hashers";
|
||||||
|
|
||||||
export class ClusterRoleBindingsStore extends KubeObjectStore<ClusterRoleBinding> {
|
export class ClusterRoleBindingsStore extends KubeObjectStore<ClusterRoleBinding> {
|
||||||
api = clusterRoleBindingApi;
|
api = clusterRoleBindingApi;
|
||||||
@ -41,28 +40,22 @@ export class ClusterRoleBindingsStore extends KubeObjectStore<ClusterRoleBinding
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSubjects(params: {
|
async updateSubjects(clusterRoleBinding: ClusterRoleBinding, subjects: ClusterRoleBindingSubject[]) {
|
||||||
clusterRoleBinding: ClusterRoleBinding;
|
|
||||||
addSubjects?: ClusterRoleBindingSubject[];
|
|
||||||
removeSubjects?: ClusterRoleBindingSubject[];
|
|
||||||
}) {
|
|
||||||
const { clusterRoleBinding, addSubjects, removeSubjects } = params;
|
|
||||||
const currentSubjects = clusterRoleBinding.getSubjects();
|
|
||||||
let newSubjects = currentSubjects;
|
|
||||||
|
|
||||||
if (addSubjects) {
|
|
||||||
newSubjects = uniqBy(currentSubjects.concat(addSubjects), ({ kind, name }) => {
|
|
||||||
return [kind, name].join("-");
|
|
||||||
});
|
|
||||||
} else if (removeSubjects) {
|
|
||||||
newSubjects = difference(currentSubjects, removeSubjects);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.update(clusterRoleBinding, {
|
return this.update(clusterRoleBinding, {
|
||||||
roleRef: clusterRoleBinding.roleRef,
|
roleRef: clusterRoleBinding.roleRef,
|
||||||
subjects: newSubjects
|
subjects,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeSubjects(clusterRoleBinding: ClusterRoleBinding, subjectsToRemove: Iterable<ClusterRoleBindingSubject>) {
|
||||||
|
const currentSubjects = new HashSet(clusterRoleBinding.getSubjects(), hashClusterRoleBindingSubject);
|
||||||
|
|
||||||
|
for (const subject of subjectsToRemove) {
|
||||||
|
currentSubjects.delete(subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.updateSubjects(clusterRoleBinding, currentSubjects.toJSON());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clusterRoleBindingsStore = new ClusterRoleBindingsStore();
|
export const clusterRoleBindingsStore = new ClusterRoleBindingsStore();
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
import "./add-dialog.scss";
|
import "./add-dialog.scss";
|
||||||
|
|
||||||
import { observable } from "mobx";
|
import { makeObservable, observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
@ -41,6 +41,11 @@ export class AddClusterRoleDialog extends React.Component<Props> {
|
|||||||
|
|
||||||
@observable clusterRoleName = "";
|
@observable clusterRoleName = "";
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
static open() {
|
static open() {
|
||||||
AddClusterRoleDialog.isOpen.set(true);
|
AddClusterRoleDialog.isOpen.set(true);
|
||||||
}
|
}
|
||||||
@ -67,7 +72,6 @@ export class AddClusterRoleDialog extends React.Component<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { ...dialogProps } = this.props;
|
const { ...dialogProps } = this.props;
|
||||||
const header = <h5>Create ClusterRole</h5>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -76,7 +80,10 @@ export class AddClusterRoleDialog extends React.Component<Props> {
|
|||||||
isOpen={AddClusterRoleDialog.isOpen.get()}
|
isOpen={AddClusterRoleDialog.isOpen.get()}
|
||||||
close={AddClusterRoleDialog.close}
|
close={AddClusterRoleDialog.close}
|
||||||
>
|
>
|
||||||
<Wizard header={header} done={AddClusterRoleDialog.close}>
|
<Wizard
|
||||||
|
header={<h5>Create ClusterRole</h5>}
|
||||||
|
done={AddClusterRoleDialog.close}
|
||||||
|
>
|
||||||
<WizardStep
|
<WizardStep
|
||||||
contentClass="flex gaps column"
|
contentClass="flex gaps column"
|
||||||
nextLabel="Create"
|
nextLabel="Create"
|
||||||
|
|||||||
@ -37,20 +37,11 @@ import { Table, TableCell, TableHead, TableRow } from "../../table";
|
|||||||
import { RoleBindingDialog } from "./dialog";
|
import { RoleBindingDialog } from "./dialog";
|
||||||
import { roleBindingsStore } from "./store";
|
import { roleBindingsStore } from "./store";
|
||||||
import { ObservableHashSet } from "../../../../common/utils/hash-set";
|
import { ObservableHashSet } from "../../../../common/utils/hash-set";
|
||||||
import { MD5 } from "crypto-js";
|
import { hashRoleBindingSubject } from "./hashers";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<RoleBinding> {
|
interface Props extends KubeObjectDetailsProps<RoleBinding> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hashRoleBindingSubject(subject: RoleBindingSubject): string {
|
|
||||||
return MD5(JSON.stringify([
|
|
||||||
["kind", subject.kind],
|
|
||||||
["name", subject.name],
|
|
||||||
["namespace", subject.namespace],
|
|
||||||
["apiGroup", subject.apiGroup],
|
|
||||||
])).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class RoleBindingDetails extends React.Component<Props> {
|
export class RoleBindingDetails extends React.Component<Props> {
|
||||||
selectedSubjects = new ObservableHashSet<RoleBindingSubject>([], hashRoleBindingSubject);
|
selectedSubjects = new ObservableHashSet<RoleBindingSubject>([], hashRoleBindingSubject);
|
||||||
@ -69,7 +60,7 @@ export class RoleBindingDetails extends React.Component<Props> {
|
|||||||
const { selectedSubjects } = this;
|
const { selectedSubjects } = this;
|
||||||
|
|
||||||
ConfirmDialog.open({
|
ConfirmDialog.open({
|
||||||
ok: () => roleBindingsStore.updateSubjects({ roleBinding, removeSubjects: selectedSubjects.toJSON() }),
|
ok: () => roleBindingsStore.removeSubjects(roleBinding, selectedSubjects.toJSON()),
|
||||||
labelOk: `Remove`,
|
labelOk: `Remove`,
|
||||||
message: (
|
message: (
|
||||||
<p>Remove selected bindings for <b>{roleBinding.getName()}</b>?</p>
|
<p>Remove selected bindings for <b>{roleBinding.getName()}</b>?</p>
|
||||||
@ -110,7 +101,7 @@ export class RoleBindingDetails extends React.Component<Props> {
|
|||||||
<Table selectable className="bindings box grow">
|
<Table selectable className="bindings box grow">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableCell checkbox />
|
<TableCell checkbox />
|
||||||
<TableCell className="binding">Binding</TableCell>
|
<TableCell className="binding">Name</TableCell>
|
||||||
<TableCell className="type">Type</TableCell>
|
<TableCell className="type">Type</TableCell>
|
||||||
<TableCell className="type">Namespace</TableCell>
|
<TableCell className="type">Namespace</TableCell>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@ -123,10 +114,7 @@ export class RoleBindingDetails extends React.Component<Props> {
|
|||||||
<TableRow
|
<TableRow
|
||||||
key={i}
|
key={i}
|
||||||
selected={isSelected}
|
selected={isSelected}
|
||||||
onClick={prevDefault(() => {
|
onClick={prevDefault(() => this.selectedSubjects.toggle(subject))}
|
||||||
console.log("toggling", subject);
|
|
||||||
this.selectedSubjects.toggle(subject);
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<TableCell checkbox isChecked={isSelected} />
|
<TableCell checkbox isChecked={isSelected} />
|
||||||
<TableCell className="binding">{name}</TableCell>
|
<TableCell className="binding">{name}</TableCell>
|
||||||
|
|||||||
@ -172,10 +172,7 @@ export class RoleBindingDialog extends React.Component<Props> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const roleBinding = this.isEditing
|
const roleBinding = this.isEditing
|
||||||
? await roleBindingsStore.updateSubjects({
|
? await roleBindingsStore.updateSubjects(this.roleBinding, selectedBindings)
|
||||||
roleBinding: this.roleBinding,
|
|
||||||
addSubjects: selectedBindings,
|
|
||||||
})
|
|
||||||
: await roleBindingsStore.create({ name: this.bindingName, namespace }, {
|
: await roleBindingsStore.create({ name: this.bindingName, namespace }, {
|
||||||
subjects: selectedBindings,
|
subjects: selectedBindings,
|
||||||
roleRef: {
|
roleRef: {
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MD5 } from "crypto-js";
|
||||||
|
import type { RoleBindingSubject } from "../../../api/endpoints";
|
||||||
|
|
||||||
|
export function hashRoleBindingSubject(subject: RoleBindingSubject): string {
|
||||||
|
return MD5(JSON.stringify([
|
||||||
|
["kind", subject.kind],
|
||||||
|
["name", subject.name],
|
||||||
|
["namespace", subject.namespace],
|
||||||
|
["apiGroup", subject.apiGroup],
|
||||||
|
])).toString();
|
||||||
|
}
|
||||||
@ -18,12 +18,12 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import difference from "lodash/difference";
|
|
||||||
import uniqBy from "lodash/uniqBy";
|
|
||||||
|
|
||||||
import { apiManager } from "../../../api/api-manager";
|
import { apiManager } from "../../../api/api-manager";
|
||||||
import { RoleBinding, roleBindingApi, RoleBindingSubject } from "../../../api/endpoints";
|
import { RoleBinding, roleBindingApi, RoleBindingSubject } from "../../../api/endpoints";
|
||||||
import { KubeObjectStore } from "../../../kube-object.store";
|
import { KubeObjectStore } from "../../../kube-object.store";
|
||||||
|
import { HashSet } from "../../../utils";
|
||||||
|
import { hashRoleBindingSubject } from "./hashers";
|
||||||
|
|
||||||
export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
||||||
api = roleBindingApi;
|
api = roleBindingApi;
|
||||||
@ -39,28 +39,22 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
|||||||
return roleBindingApi.create(params, data);
|
return roleBindingApi.create(params, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSubjects(params: {
|
async updateSubjects(roleBinding: RoleBinding, subjects: RoleBindingSubject[]) {
|
||||||
roleBinding: RoleBinding;
|
|
||||||
addSubjects?: RoleBindingSubject[];
|
|
||||||
removeSubjects?: RoleBindingSubject[];
|
|
||||||
}) {
|
|
||||||
const { roleBinding, addSubjects, removeSubjects } = params;
|
|
||||||
const currentSubjects = roleBinding.getSubjects();
|
|
||||||
let newSubjects = currentSubjects;
|
|
||||||
|
|
||||||
if (addSubjects) {
|
|
||||||
newSubjects = uniqBy(currentSubjects.concat(addSubjects), ({ kind, name, namespace }) => {
|
|
||||||
return [kind, name, namespace].join("-");
|
|
||||||
});
|
|
||||||
} else if (removeSubjects) {
|
|
||||||
newSubjects = difference(currentSubjects, removeSubjects);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.update(roleBinding, {
|
return this.update(roleBinding, {
|
||||||
roleRef: roleBinding.roleRef,
|
roleRef: roleBinding.roleRef,
|
||||||
subjects: newSubjects
|
subjects,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeSubjects(roleBinding: RoleBinding, subjectsToRemove: Iterable<RoleBindingSubject>) {
|
||||||
|
const currentSubjects = new HashSet(roleBinding.getSubjects(), hashRoleBindingSubject);
|
||||||
|
|
||||||
|
for (const subject of subjectsToRemove) {
|
||||||
|
currentSubjects.delete(subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.updateSubjects(roleBinding, currentSubjects.toJSON());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const roleBindingsStore = new RoleBindingsStore();
|
export const roleBindingsStore = new RoleBindingsStore();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user