diff --git a/src/common/utils/__tests__/hash-set.test.ts b/src/common/utils/__tests__/hash-set.test.ts index 96e2f2502a..38614b7d3e 100644 --- a/src/common/utils/__tests__/hash-set.test.ts +++ b/src/common/utils/__tests__/hash-set.test.ts @@ -19,7 +19,7 @@ * 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", () => { it("should not throw on creation", () => { @@ -273,3 +273,256 @@ describe("ObservableHashSet", () => { }); }); }); + +describe("HashSet", () => { + 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, + }); + }); +}); diff --git a/src/common/utils/hash-set.ts b/src/common/utils/hash-set.ts index 003f687ef6..87fa8daa4c 100644 --- a/src/common/utils/hash-set.ts +++ b/src/common/utils/hash-set.ts @@ -27,6 +27,117 @@ export function makeIterableIterator(iterator: Iterator): IterableIterator return iterator as IterableIterator; } +export class HashSet implements Set { + #hashmap: Map; + + constructor(initialValues: Iterable, protected hasher: (item: T) => string) { + this.#hashmap = new Map(Array.from(initialValues, value => [this.hasher(value), value])); + } + + replace(other: ObservableHashSet | ObservableSet | Set | 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) => 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 { + return this.values(); + } + + values(): IterableIterator { + let nextIndex = 0; + const observableValues = Array.from(this.#hashmap.values()); + + return makeIterableIterator({ + next: () => { + return nextIndex < observableValues.length + ? { value: observableValues[nextIndex++], done: false } + : { done: true, value: undefined }; + } + }); + } + + [Symbol.iterator](): IterableIterator { + return this.#hashmap.values(); + } + + get [Symbol.toStringTag](): string { + return "Set"; + } + + toJSON(): T[] { + return Array.from(this); + } + + toString(): string { + return "[object Set]"; + } +} + export class ObservableHashSet implements Set, IInterceptable, IListenable { #hashmap: ObservableMap; @@ -44,16 +155,20 @@ export class ObservableHashSet implements Set, IInterceptable | ObservableSet | Set | readonly T[]): this { - if (Array.isArray(other)) { - this.clear(); - other.forEach(value => this.add(value)); - } else if (other instanceof Set || other instanceof ObservableHashSet || other instanceof ObservableSet) { - this.clear(); - other.forEach(value => this.add(value)); - } else if (other !== null && other !== undefined) { + 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; } diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/details.tsx b/src/renderer/components/+user-management/+cluster-role-bindings/details.tsx index 51c43eb6ab..ac5b2cb933 100644 --- a/src/renderer/components/+user-management/+cluster-role-bindings/details.tsx +++ b/src/renderer/components/+user-management/+cluster-role-bindings/details.tsx @@ -21,14 +21,14 @@ import "./details.scss"; -import { observable, reaction } from "mobx"; +import { reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import React from "react"; import { KubeEventDetails } from "../../+events/kube-event-details"; import type { ClusterRoleBinding, ClusterRoleBindingSubject } from "../../../api/endpoints"; 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 { ConfirmDialog } from "../../confirm-dialog"; import { DrawerTitle } from "../../drawer"; @@ -37,13 +37,14 @@ import { KubeObjectMeta } from "../../kube-object/kube-object-meta"; import { Table, TableCell, TableHead, TableRow } from "../../table"; import { ClusterRoleBindingDialog } from "./dialog"; import { clusterRoleBindingsStore } from "./store"; +import { hashClusterRoleBindingSubject } from "./hashers"; interface Props extends KubeObjectDetailsProps { } @observer export class ClusterRoleBindingDetails extends React.Component { - @observable selectedSubjects = observable.array([], { deep: false }); + selectedSubjects = new ObservableHashSet([], hashClusterRoleBindingSubject); constructor(props: Props) { super(props); @@ -58,23 +59,12 @@ export class ClusterRoleBindingDetails extends React.Component { ]); } - 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() { const { object: clusterRoleBinding } = this.props; const { selectedSubjects } = this; ConfirmDialog.open({ - ok: () => clusterRoleBindingsStore.updateSubjects({ clusterRoleBinding, removeSubjects: selectedSubjects }), + ok: () => clusterRoleBindingsStore.removeSubjects(clusterRoleBinding, selectedSubjects), labelOk: `Remove`, message: (

Remove selected bindings for {clusterRoleBinding.getName()}?

@@ -115,19 +105,19 @@ export class ClusterRoleBindingDetails extends React.Component { - Binding + Name Type { subjects.map((subject, i) => { const { kind, name } = subject; - const isSelected = selectedSubjects.includes(subject); + const isSelected = selectedSubjects.has(subject); return ( this.selectSubject(subject))} + onClick={prevDefault(() => this.selectedSubjects.toggle(subject))} > {name} @@ -141,7 +131,7 @@ export class ClusterRoleBindingDetails extends React.Component { ClusterRoleBindingDialog.open(clusterRoleBinding)} - onRemove={selectedSubjects.length ? this.removeSelectedSubjects : null} + onRemove={selectedSubjects.size ? this.removeSelectedSubjects : null} addTooltip={`Add bindings to ${roleRef.name}`} removeTooltip={`Remove selected bindings from ${roleRef.name}`} /> diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx b/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx index b39d891914..87882b8933 100644 --- a/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx +++ b/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx @@ -170,10 +170,7 @@ export class ClusterRoleBindingDialog extends React.Component { try { const { selfLink } = this.isEditing - ? await clusterRoleBindingsStore.updateSubjects({ - clusterRoleBinding: this.clusterRoleBinding, - addSubjects: selectedBindings, - }) + ? await clusterRoleBindingsStore.updateSubjects(this.clusterRoleBinding, selectedBindings) : await clusterRoleBindingsStore.create({ name: bindingName }, { subjects: selectedBindings, roleRef: { @@ -238,16 +235,11 @@ export class ClusterRoleBindingDialog extends React.Component { render() { const { ...dialogProps } = this.props; - const { isEditing, clusterRoleBinding: roleBinding, selectedRoleRef, selectedBindings } = this; - const roleBindingName = roleBinding ? roleBinding.getName() : ""; - const header = ( -
- {roleBindingName - ? <>Edit ClusterRoleBinding {roleBindingName} - : "Add ClusterRoleBinding" - } -
- ); + const { isEditing, clusterRoleBinding, selectedRoleRef, selectedBindings } = this; + const clusterRoleBindingName = clusterRoleBinding?.getName(); + const header = clusterRoleBindingName + ?
Edit ClusterRoleBinding {clusterRoleBindingName}
+ :
Add ClusterRoleBinding
; const disableNext = !selectedRoleRef || !selectedBindings.length; const nextLabel = isEditing ? "Update" : "Create"; diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/hashers.ts b/src/renderer/components/+user-management/+cluster-role-bindings/hashers.ts new file mode 100644 index 0000000000..9bb85576ca --- /dev/null +++ b/src/renderer/components/+user-management/+cluster-role-bindings/hashers.ts @@ -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(); +} diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/store.ts b/src/renderer/components/+user-management/+cluster-role-bindings/store.ts index f70ac9e611..56fce615e0 100644 --- a/src/renderer/components/+user-management/+cluster-role-bindings/store.ts +++ b/src/renderer/components/+user-management/+cluster-role-bindings/store.ts @@ -18,13 +18,12 @@ * 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 difference from "lodash/difference"; -import uniqBy from "lodash/uniqBy"; import { apiManager } from "../../../api/api-manager"; import { ClusterRoleBinding, clusterRoleBindingApi, ClusterRoleBindingSubject } from "../../../api/endpoints"; import { KubeObjectStore } from "../../../kube-object.store"; -import { autoBind } from "../../../utils"; +import { autoBind, HashSet } from "../../../utils"; +import { hashClusterRoleBindingSubject } from "./hashers"; export class ClusterRoleBindingsStore extends KubeObjectStore { api = clusterRoleBindingApi; @@ -41,28 +40,22 @@ export class ClusterRoleBindingsStore extends KubeObjectStore { - return [kind, name].join("-"); - }); - } else if (removeSubjects) { - newSubjects = difference(currentSubjects, removeSubjects); - } - + async updateSubjects(clusterRoleBinding: ClusterRoleBinding, subjects: ClusterRoleBindingSubject[]) { return this.update(clusterRoleBinding, { roleRef: clusterRoleBinding.roleRef, - subjects: newSubjects + subjects, }); } + + async removeSubjects(clusterRoleBinding: ClusterRoleBinding, subjectsToRemove: Iterable) { + 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(); diff --git a/src/renderer/components/+user-management/+cluster-roles/add-dialog.tsx b/src/renderer/components/+user-management/+cluster-roles/add-dialog.tsx index a238dd942e..4006334903 100644 --- a/src/renderer/components/+user-management/+cluster-roles/add-dialog.tsx +++ b/src/renderer/components/+user-management/+cluster-roles/add-dialog.tsx @@ -20,7 +20,7 @@ */ import "./add-dialog.scss"; -import { observable } from "mobx"; +import { makeObservable, observable } from "mobx"; import { observer } from "mobx-react"; import React from "react"; @@ -41,6 +41,11 @@ export class AddClusterRoleDialog extends React.Component { @observable clusterRoleName = ""; + constructor(props: Props) { + super(props); + makeObservable(this); + } + static open() { AddClusterRoleDialog.isOpen.set(true); } @@ -67,7 +72,6 @@ export class AddClusterRoleDialog extends React.Component { render() { const { ...dialogProps } = this.props; - const header =
Create ClusterRole
; return ( { isOpen={AddClusterRoleDialog.isOpen.get()} close={AddClusterRoleDialog.close} > - + Create ClusterRole} + done={AddClusterRoleDialog.close} + > { } -function hashRoleBindingSubject(subject: RoleBindingSubject): string { - return MD5(JSON.stringify([ - ["kind", subject.kind], - ["name", subject.name], - ["namespace", subject.namespace], - ["apiGroup", subject.apiGroup], - ])).toString(); -} - @observer export class RoleBindingDetails extends React.Component { selectedSubjects = new ObservableHashSet([], hashRoleBindingSubject); @@ -69,7 +60,7 @@ export class RoleBindingDetails extends React.Component { const { selectedSubjects } = this; ConfirmDialog.open({ - ok: () => roleBindingsStore.updateSubjects({ roleBinding, removeSubjects: selectedSubjects.toJSON() }), + ok: () => roleBindingsStore.removeSubjects(roleBinding, selectedSubjects.toJSON()), labelOk: `Remove`, message: (

Remove selected bindings for {roleBinding.getName()}?

@@ -110,7 +101,7 @@ export class RoleBindingDetails extends React.Component {
- Binding + Name Type Namespace @@ -123,10 +114,7 @@ export class RoleBindingDetails extends React.Component { { - console.log("toggling", subject); - this.selectedSubjects.toggle(subject); - })} + onClick={prevDefault(() => this.selectedSubjects.toggle(subject))} > {name} diff --git a/src/renderer/components/+user-management/+role-bindings/dialog.tsx b/src/renderer/components/+user-management/+role-bindings/dialog.tsx index ba7113ed10..ce76871eba 100644 --- a/src/renderer/components/+user-management/+role-bindings/dialog.tsx +++ b/src/renderer/components/+user-management/+role-bindings/dialog.tsx @@ -172,10 +172,7 @@ export class RoleBindingDialog extends React.Component { try { const roleBinding = this.isEditing - ? await roleBindingsStore.updateSubjects({ - roleBinding: this.roleBinding, - addSubjects: selectedBindings, - }) + ? await roleBindingsStore.updateSubjects(this.roleBinding, selectedBindings) : await roleBindingsStore.create({ name: this.bindingName, namespace }, { subjects: selectedBindings, roleRef: { diff --git a/src/renderer/components/+user-management/+role-bindings/hashers.ts b/src/renderer/components/+user-management/+role-bindings/hashers.ts new file mode 100644 index 0000000000..2d8771bb18 --- /dev/null +++ b/src/renderer/components/+user-management/+role-bindings/hashers.ts @@ -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(); +} diff --git a/src/renderer/components/+user-management/+role-bindings/store.ts b/src/renderer/components/+user-management/+role-bindings/store.ts index f2a3f65946..d59db292e6 100644 --- a/src/renderer/components/+user-management/+role-bindings/store.ts +++ b/src/renderer/components/+user-management/+role-bindings/store.ts @@ -18,12 +18,12 @@ * 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 difference from "lodash/difference"; -import uniqBy from "lodash/uniqBy"; import { apiManager } from "../../../api/api-manager"; import { RoleBinding, roleBindingApi, RoleBindingSubject } from "../../../api/endpoints"; import { KubeObjectStore } from "../../../kube-object.store"; +import { HashSet } from "../../../utils"; +import { hashRoleBindingSubject } from "./hashers"; export class RoleBindingsStore extends KubeObjectStore { api = roleBindingApi; @@ -39,28 +39,22 @@ export class RoleBindingsStore extends KubeObjectStore { return roleBindingApi.create(params, data); } - async updateSubjects(params: { - 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); - } - + async updateSubjects(roleBinding: RoleBinding, subjects: RoleBindingSubject[]) { return this.update(roleBinding, { roleRef: roleBinding.roleRef, - subjects: newSubjects + subjects, }); } + + async removeSubjects(roleBinding: RoleBinding, subjectsToRemove: Iterable) { + 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();