Turn on strict mode in tsconfig.json, some helpful lints, and required cleanup where strictness necessitates it (#5195)

* Turn on strict mode in tsconfig.json

- Add route, clusterRoute, and payloadValidatedClusterRoute helper
  functions to improve types with backend routes

- Turn on the following new lints:
  - react/jsx-first-prop-new-line
  - react/jsx-wrap-multilines
  - react/jsx-one-expression-per-line
  - react/jsx-max-props-per-line
  - react/jsx-indent
  - react/jsx-indent-props

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

* fix build

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

* Replace KubeObject scope strings with enum

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

* Revert package.json version changes

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

* revert move hostedCluster(Id)

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

* change some type param names to be not single letters

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

* remove copy-extension-themes

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

* add new make clean action

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

* fix build to not use webpack for generating types

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

* fix kube-object-menu.test.tsx

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

* fix select.test.tsx

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

* fix catalog.test.tsx

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

* revert move fileNameMigration to index

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

* fix ref logic error

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

* fix log-resource-selector.test.tsx tests

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

* fix dock-store.test.ts test by overriding createStorage to not touch file system

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

* fix cluster.test.ts tests

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

* fix kube=api.test.ts

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

* fixed hotbar-store.test.ts

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

* fix kubeconfig-manager.test.ts

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

* fix cluster-role-bindings/__tests__/dialog.test.tsx

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

* fix role-bindings/__tests__/dialog.test.tsx

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

* fix pods.test.ts

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

* fix delete-cluster-dialog.test.tsx

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

* fix daemonset.store.test.ts

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

* fix replicaset.store.test.ts

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

* fix statefulsets/dialog/dialog.test.tsx

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

* fix replicasets/scale-dialog/dialog.test.tsx

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

* fix deployments.store.test.ts

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

* fix deployments/scale/dialog.test.tsx

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

* fix cronjob.store.test.ts

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

* fix stateful-set.api.test.ts

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

* fix deployment.api.test.ts

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

* fix api-manager.test.ts

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

* fix statefulset.store.test.ts

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

* fix job.store.test.ts

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

* fix pods.store.test.ts

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

* fix scroll-spy.test.tsx

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

* fix hotbar-remove-command.test.tsx

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

* fix catalog-entity-registry.test.ts

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

* fix welcome.test.tsx

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

* fix verify-that-all-routes-have-route-component.test.ts

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

* fix pod-tolerations.test.tsx

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

* better fix for previous 3 fixes, plus also select.test.tsx

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

* fix kube-object-menu.test.tsx

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

* fix app-paths.test.ts

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

* fix dock-tabs.test.tsx

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

* fix isReactNode typing

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

* fix sub-title.tsx

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

* fix drawer.tsx

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

* fix list-layout.tsx and header.tsx

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

* fix error-boundary.tsx

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

* fix upgrade-chart/store.ts and dock-tab.store.ts

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

* fix install-chart/store.ts

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

* fix edit-resource/store.ts

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

* fix create-resource/store.ts

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

* fix namespace-select.tsx

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

* fix namespace-select-filter.tsx

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

* fix crd-list.tsx

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

* fix wrong types for extensions

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

* fix circular dependency

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

* fix circular dependency on catalogCategoryRegistry

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

* fix api-kube

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

* fix type errors, most <Select /> errors

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

* fixing more type errors

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

* some more fixing type errors

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

* convert all KubeApis to injectable with legacy global backups

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

* factor out into a common file all the exports

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

* convert all KubeObjectStores to injectable with legacy global backups

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

* fix lint

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

* remove unused legacy KubeApi globals

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

* fix bad previous commit

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

* more crash fixing

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

* try and fix behavioural tests

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

* fix sidebar-and-tab-navigation-for-core.test.tsx

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

* fix sidebar-and-tab-navigation-for-extensions.test.tsx

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

* fix navigation-using-application-menu.test.ts

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

* fix catalog.test.tsx

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

* Make ThemeStore non-singleton and fix navigation-to-terminal-preferences.test.ts

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

* extensions.test.tsx

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

* fix catalog-entity-registry.test.ts

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

* fix navigation-using-application-menu.test.ts

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

* fix log-resource-selector.test.tsx

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

* fix dock-tabs.test.tsx

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

* fix delete-cluster-dialog.test.tsx

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

* fix navigation-to-kubernetes-preferences.test.ts

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

* fix navigation-to-editor-preferences.test.ts

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

* fix navigation-to-proxy-preferences.test.ts

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

* fix navigation-using-application-menu.test.ts

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

* fix navigation-to-application-preferences.test.ts

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

* fix dock-store.test.ts

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

* fix select.test.tsx

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

* fix role-bindings/__tests__/dialog.test.tsx

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

* fix hotbar-remove-command.test.tsx

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

* fix cluster-role-bindings/__tests__/dialog.test.tsx

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

* fix navigation-to-extension-specific-preferences.test.tsx

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

* fix navigation-to-telemetry-preferences.test.tsx

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

* fix closing-preferences.test.tsx

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

* fix navigation-to-editor-preferences.test.ts

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

* fix navigation-to-proxy-preferences.test.ts

- Fix other type errors too

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

* final tweaks

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

* Add more tsconfig files, fix bug in <Catalog>

- Make all of history, navigation injectable

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

* fix type errors

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

* Convert all of kube-details-params/ and navigate/ to injectable

- This fixes a runtime error that was encountered during testing

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

* Fix runtime errors on renderer

- remove all static uses of `createPageParam` (and then removed the
  legacy global)
- Made LensRendererExtension and LensMainExtension just used
  dependencies and not the getLegacyDi
- Fixed circular dep in extension-loader

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

* Move registerStore calls to after injectMany

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

* replace all the rest of the legacy uses of apiManager

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

* fix stack overflow and cycles in DI

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

* fix NamespaceSelectFilter not opening

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

* fix WizardStep and AddNamespaceDialog

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

* fix KubeApi's not being registered

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

* cleanup WindowManager

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

* Proper fix for Wizard, fix NamespaceStore.subscribe

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

* Rewrite withTooltip to be more type correct

- Fixes mobx related "too many recursive actions" error

- Change all the uses of withTooltips to be functional components

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

* Add e2e test to cover kube api registration

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

* cleanup internal-commands

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

* remove cast in <Animate>

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

* Fix command-palette e2e test

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

* Fix type error after rebase

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

* Fix test name

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

* fix lint

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

* fix code to help CodeQL scanner

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

* update intree extension lock files

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

* Fix build-extensions picking wrong @types/react

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

* fix tests from rebase

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

* fix type error

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

* Make KubeconfigSyncManager more injectable

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

* fix crash in test mode for Dialog

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

* make Select snapshots deterministic

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

* fix new type error

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

* fix kube-object.store.test.ts typing

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

* fix merge build issues

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

* fix snapshots after merge

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

* fix lint after merge

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

* reexport BaseKubeJsonApiObjectMetadata

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

* fix typo in terminalSpawningPool

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

* remove duplicate license header

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

* fix typo to waitUntilDefined

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

* remove iter use from getLegacyGlobalDiForExtensionApi

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

* remove complex createStorage override

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

* override logger with mocks only when needed for tests

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

* remove specialized overrideStore flags for getDiForUnitTesting

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

* remove unnecessary | undefined types from the exactOptionalFieldTypes experiment

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

* use more descriptive name for local test mocks

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

* remove unnecessary addition to 'make clean' target

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

* remove oddity of KubeObjectStore.getById(undefined) being allowed

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

* rename KubeObject.getDescriptor in favour of name without fundemental JS meaning

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

* Simplify legacyRegisterApi when working in behaviour unit tests

- Don't emit within main environment as there should be no auto
  registering there

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

* change confusing variable name in ReactiveDuration

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

* make visitor pattern more explicit for Entity contextMenuOpen

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

* toggleDetails -> toggleKubeDetailsPane is more specific

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

* remove outdated comment

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

* fix bug where LensExtension dependencies are not set

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

* Fix tests from the revert of react 18

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

* fix more tests from merge

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

* fix typings with new is-compatible-extension tests

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

* more type fixing

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

* Revert in-tree extension versions

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

* Improve name of guarding injectable for stores and apis

- New name better implies that it is just a guard state and does not do
  anything

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

* Add helper for <Select>.isMulti for storing in a Set<Value>

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

* fix is-compatible-extension.test.ts types

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2022-05-16 07:17:57 -04:00
1 parent 381d77c633
commit dfcb7c3330
1062 files changed
+26512 -28689

No files matched your search

@@ -5,14 +5,14 @@
import React from "react";
import { ClusterRoleBindingDialog } from "../dialog";
import { clusterRolesStore } from "../../+cluster-roles/store";
import { ClusterRole } from "../../../../../common/k8s-api/endpoints";
import userEvent from "@testing-library/user-event";
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
import type { DiRender } from "../../../test-utils/renderFor";
import { renderFor } from "../../../test-utils/renderFor";
jest.mock("../../+cluster-roles/store");
import clusterRoleStoreInjectable from "../../+cluster-roles/store.injectable";
import storesAndApisCanBeCreatedInjectable from "../../../../stores-apis-can-be-created.injectable";
import ipcRendererInjectable from "../../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
describe("ClusterRoleBindingDialog tests", () => {
let render: DiRender;
@@ -20,19 +20,30 @@ describe("ClusterRoleBindingDialog tests", () => {
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(storesAndApisCanBeCreatedInjectable, () => true);
di.override(ipcRendererInjectable, () => ({
on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
await di.runSetups();
render = renderFor(di);
(clusterRolesStore as any).items = [new ClusterRole({
apiVersion: "rbac.authorization.k8s.io/v1",
kind: "ClusterRole",
metadata: {
name: "foobar",
resourceVersion: "1",
uid: "1",
},
})];
const store = di.inject(clusterRoleStoreInjectable);
store.items.replace([
new ClusterRole({
apiVersion: "rbac.authorization.k8s.io/v1",
kind: "ClusterRole",
metadata: {
name: "foobar",
resourceVersion: "1",
uid: "1",
selfLink: "/apis/rbac.authorization.k8s.io/v1/clusterroles/foobar",
},
}),
]);
});
afterEach(() => {
@@ -9,7 +9,7 @@ import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import type { ClusterRoleBinding, ClusterRoleBindingSubject } from "../../../../common/k8s-api/endpoints";
import type { ClusterRoleBinding } from "../../../../common/k8s-api/endpoints";
import { autoBind, ObservableHashSet, prevDefault } from "../../../utils";
import { AddRemoveButtons } from "../../add-remove-buttons";
import { DrawerTitle } from "../../drawer";
@@ -17,8 +17,8 @@ import type { KubeObjectDetailsProps } from "../../kube-object-details";
import { KubeObjectMeta } from "../../kube-object-meta";
import { Table, TableCell, TableHead, TableRow } from "../../table";
import { ClusterRoleBindingDialog } from "./dialog";
import { clusterRoleBindingsStore } from "./store";
import { hashClusterRoleBindingSubject } from "./hashers";
import { clusterRoleBindingStore } from "./legacy-store";
import { hashSubject } from "../hashers";
import type { OpenConfirmDialog } from "../../confirm-dialog/open.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import openConfirmDialogInjectable from "../../confirm-dialog/open.injectable";
@@ -32,7 +32,7 @@ interface Dependencies {
@observer
class NonInjectedClusterRoleBindingDetails extends React.Component<ClusterRoleBindingDetailsProps & Dependencies> {
selectedSubjects = new ObservableHashSet<ClusterRoleBindingSubject>([], hashClusterRoleBindingSubject);
selectedSubjects = new ObservableHashSet([], hashSubject);
constructor(props: ClusterRoleBindingDetailsProps & Dependencies) {
super(props);
@@ -52,10 +52,14 @@ class NonInjectedClusterRoleBindingDetails extends React.Component<ClusterRoleBi
const { selectedSubjects } = this;
openConfirmDialog({
ok: () => clusterRoleBindingsStore.removeSubjects(clusterRoleBinding, selectedSubjects),
ok: () => clusterRoleBindingStore.removeSubjects(clusterRoleBinding, selectedSubjects),
labelOk: `Remove`,
message: (
<p>Remove selected bindings for <b>{clusterRoleBinding.getName()}</b>?</p>
<p>
Remove selected bindings for
<b>{clusterRoleBinding.getName()}</b>
?
</p>
),
});
}
@@ -121,7 +125,7 @@ class NonInjectedClusterRoleBindingDetails extends React.Component<ClusterRoleBi
<AddRemoveButtons
onAdd={() => ClusterRoleBindingDialog.open(clusterRoleBinding)}
onRemove={selectedSubjects.size ? this.removeSelectedSubjects : null}
onRemove={selectedSubjects.size ? this.removeSelectedSubjects : undefined}
addTooltip={`Add bindings to ${roleRef.name}`}
removeTooltip={`Remove selected bindings from ${roleRef.name}`}
/>
@@ -9,8 +9,8 @@ import { action, computed, makeObservable, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import { serviceAccountsStore } from "../+service-accounts/store";
import type { ClusterRole, ClusterRoleBinding, ClusterRoleBindingSubject, ServiceAccount } from "../../../../common/k8s-api/endpoints";
import { serviceAccountStore } from "../+service-accounts/legacy-store";
import type { ClusterRole, ClusterRoleBinding, ServiceAccount } from "../../../../common/k8s-api/endpoints";
import type { DialogProps } from "../../dialog";
import { Dialog } from "../../dialog";
import { EditableList } from "../../editable-list";
@@ -18,14 +18,14 @@ import { Icon } from "../../icon";
import { showDetails } from "../../kube-detail-params";
import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications";
import type { SelectOption } from "../../select";
import { Select } from "../../select";
import { onMultiSelectFor, Select } from "../../select";
import { Wizard, WizardStep } from "../../wizard";
import { clusterRoleBindingsStore } from "./store";
import { clusterRolesStore } from "../+cluster-roles/store";
import { clusterRoleBindingStore } from "./legacy-store";
import { clusterRoleStore } from "../+cluster-roles/legacy-store";
import { ObservableHashSet, nFircate } from "../../../utils";
import { Input } from "../../input";
import { TooltipPosition } from "../../tooltip";
import type { Subject } from "../../../../common/k8s-api/endpoints/types/subject";
export interface ClusterRoleBindingDialogProps extends Partial<DialogProps> {
}
@@ -35,12 +35,12 @@ interface DialogState {
data?: ClusterRoleBinding;
}
const dialogState = observable.object<DialogState>({
isOpen: false,
});
@observer
export class ClusterRoleBindingDialog extends React.Component<ClusterRoleBindingDialogProps> {
static state = observable.object<DialogState>({
isOpen: false,
});
constructor(props: ClusterRoleBindingDialogProps) {
super(props);
makeObservable(this);
@@ -49,25 +49,40 @@ export class ClusterRoleBindingDialog extends React.Component<ClusterRoleBinding
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => this.isEditing, () => {
this.bindingName = ClusterRoleBindingDialog.state.data?.getName();
this.bindingName = dialogState.data?.getName() ?? "";
}),
]);
}
static open(roleBinding?: ClusterRoleBinding) {
ClusterRoleBindingDialog.state.isOpen = true;
ClusterRoleBindingDialog.state.data = roleBinding;
dialogState.isOpen = true;
dialogState.data = roleBinding;
}
static close() {
ClusterRoleBindingDialog.state.isOpen = false;
dialogState.isOpen = false;
}
get clusterRoleBinding(): ClusterRoleBinding {
return ClusterRoleBindingDialog.state.data;
@computed get clusterRoleOptions() {
return clusterRoleStore.items.map(clusterRole => ({
value: clusterRole,
label: clusterRole.getName(),
}));
}
@computed get isEditing() {
@computed get serviceAccountOptions() {
return serviceAccountStore.items.map(serviceAccount => ({
value: serviceAccount,
label: `${serviceAccount.getName()} (${serviceAccount.getNs()})`,
isSelected: this.selectedAccounts.has(serviceAccount),
}));
}
get clusterRoleBinding() {
return dialogState.data;
}
get isEditing() {
return !!this.clusterRoleBinding;
}
@@ -77,7 +92,7 @@ export class ClusterRoleBindingDialog extends React.Component<ClusterRoleBinding
selectedUsers = observable.set<string>([]);
selectedGroups = observable.set<string>([]);
@computed get selectedBindings(): ClusterRoleBindingSubject[] {
@computed get selectedBindings(): Subject[] {
const serviceAccounts = Array.from(this.selectedAccounts, sa => ({
name: sa.getName(),
kind: "ServiceAccount" as const,
@@ -99,24 +114,6 @@ export class ClusterRoleBindingDialog extends React.Component<ClusterRoleBinding
];
}
@computed get clusterRoleRefoptions(): SelectOption<ClusterRole>[] {
return clusterRolesStore.items.map(value => ({
value,
label: value.getName(),
}));
}
@computed get serviceAccountOptions(): SelectOption<ServiceAccount>[] {
return serviceAccountsStore.items.map(account => ({
value: account,
label: `${account.getName()} (${account.getNs()})`,
}));
}
@computed get selectedServiceAccountOptions(): SelectOption<ServiceAccount>[] {
return this.serviceAccountOptions.filter(({ value }) => this.selectedAccounts.has(value));
}
onOpen = action(() => {
const binding = this.clusterRoleBinding;
@@ -124,16 +121,16 @@ export class ClusterRoleBindingDialog extends React.Component<ClusterRoleBinding
return this.reset();
}
this.selectedRoleRef = clusterRolesStore
this.selectedRoleRef = clusterRoleStore
.items
.find(item => item.getName() === binding.roleRef.name);
this.bindingName = this.clusterRoleBinding.getName();
this.bindingName = binding.getName();
const [saSubjects, uSubjects, gSubjects] = nFircate(binding.getSubjects(), "kind", ["ServiceAccount", "User", "Group"]);
const accountNames = new Set(saSubjects.map(acc => acc.name));
this.selectedAccounts.replace(
serviceAccountsStore.items
serviceAccountStore.items
.filter(sa => accountNames.has(sa.getName())),
);
this.selectedUsers.replace(uSubjects.map(user => user.name));
@@ -149,12 +146,16 @@ export class ClusterRoleBindingDialog extends React.Component<ClusterRoleBinding
});
createBindings = async () => {
const { selectedRoleRef, selectedBindings, bindingName } = this;
const { selectedRoleRef, selectedBindings, bindingName, clusterRoleBinding } = this;
if (!clusterRoleBinding || !selectedRoleRef) {
return;
}
try {
const { selfLink } = this.isEditing
? await clusterRoleBindingsStore.updateSubjects(this.clusterRoleBinding, selectedBindings)
: await clusterRoleBindingsStore.create({ name: bindingName }, {
? await clusterRoleBindingStore.updateSubjects(clusterRoleBinding, selectedBindings)
: await clusterRoleBindingStore.create({ name: bindingName }, {
subjects: selectedBindings,
roleRef: {
name: selectedRoleRef.getName(),
@@ -165,7 +166,7 @@ export class ClusterRoleBindingDialog extends React.Component<ClusterRoleBinding
showDetails(selfLink);
ClusterRoleBindingDialog.close();
} catch (err) {
Notifications.error(err);
Notifications.checkedError(err, `Unknown error occured while ${this.isEditing ? "editing the" : "creating a"} ClusterRoleBinding`);
}
};
@@ -178,29 +179,29 @@ export class ClusterRoleBindingDialog extends React.Component<ClusterRoleBinding
themeName="light"
placeholder="Select cluster role ..."
isDisabled={this.isEditing}
options={this.clusterRoleRefoptions}
options={this.clusterRoleOptions}
value={this.selectedRoleRef}
autoFocus={!this.isEditing}
formatOptionLabel={({ value }: SelectOption<ClusterRole>) => (
formatOptionLabel={option => (
<>
<Icon
small
material={value.kind === "Role" ? "person" : "people"}
material="people"
tooltip={{
preferredPositions: TooltipPosition.LEFT,
children: value.kind,
children: option.value.kind,
}}
/>
{" "}
{value.getName()}
{option.value.getName()}
</>
)}
onChange={({ value }: SelectOption<ClusterRole> ) => {
if (!this.selectedRoleRef || this.bindingName === this.selectedRoleRef.getName()) {
this.bindingName = value.getName();
}
onChange={option => {
this.selectedRoleRef = option?.value;
this.selectedRoleRef = value;
if (!this.selectedRoleRef || this.bindingName === this.selectedRoleRef.getName()) {
this.bindingName = option?.value?.getName() ?? "";
}
}}
/>
@@ -236,19 +237,14 @@ export class ClusterRoleBindingDialog extends React.Component<ClusterRoleBinding
isMulti
themeName="light"
placeholder="Select service accounts ..."
autoConvertOptions={false}
options={this.serviceAccountOptions}
value={this.selectedServiceAccountOptions}
formatOptionLabel={({ value }: SelectOption<ServiceAccount>) => (
<><Icon small material="account_box" /> {value.getName()} ({value.getNs()})</>
formatOptionLabel={option => (
<>
<Icon small material="account_box" />
{` ${option.label}`}
</>
)}
onChange={(selected: SelectOption<ServiceAccount>[] | null) => {
if (selected) {
this.selectedAccounts.replace(selected.map(opt => opt.value));
} else {
this.selectedAccounts.clear();
}
}}
onChange={onMultiSelectFor(this.selectedAccounts)}
maxMenuHeight={200}
/>
</>
@@ -264,13 +260,17 @@ export class ClusterRoleBindingDialog extends React.Component<ClusterRoleBinding
<Dialog
{...dialogProps}
className="AddClusterRoleBindingDialog"
isOpen={ClusterRoleBindingDialog.state.isOpen}
isOpen={dialogState.isOpen}
close={ClusterRoleBindingDialog.close}
onClose={this.reset}
onOpen={this.onOpen}
>
<Wizard
header={<h5>{action} ClusterRoleBinding</h5>}
header={(
<h5>
{`${action} ClusterRoleBinding`}
</h5>
)}
done={ClusterRoleBindingDialog.close}
>
<WizardStep
@@ -1,15 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { MD5 } from "crypto-js";
import type { ClusterRoleBindingSubject } from "../../../../common/k8s-api/endpoints";
export function hashClusterRoleBindingSubject(subject: ClusterRoleBindingSubject): string {
return MD5(JSON.stringify([
["kind", subject.kind],
["name", subject.name],
["apiGroup", subject.apiGroup],
])).toString();
}
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { asLegacyGlobalForExtensionApi } from "../../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import clusterRoleBindingStoreInjectable from "./store.injectable";
/**
* @deprecated use `di.inject(clusterRoleBindingStoreInjectable)` instead
*/
export const clusterRoleBindingStore = asLegacyGlobalForExtensionApi(clusterRoleBindingStoreInjectable);
@@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../../../../common/k8s-api/stores-apis-can-be-created.token";
import clusterRoleBindingApiInjectable from "../../../../common/k8s-api/endpoints/cluster-role-binding.api.injectable";
import { kubeObjectStoreInjectionToken } from "../../../../common/k8s-api/api-manager/manager.injectable";
import { ClusterRoleBindingStore } from "./store";
const clusterRoleBindingStoreInjectable = getInjectable({
id: "cluster-role-binding-store",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "clusterRoleBindingStore is only accessible in certain environments");
const api = di.inject(clusterRoleBindingApiInjectable);
return new ClusterRoleBindingStore(api);
},
injectionToken: kubeObjectStoreInjectionToken,
});
export default clusterRoleBindingStoreInjectable;
@@ -3,21 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { apiManager } from "../../../../common/k8s-api/api-manager";
import type { ClusterRoleBinding, ClusterRoleBindingSubject } from "../../../../common/k8s-api/endpoints";
import { clusterRoleBindingApi } from "../../../../common/k8s-api/endpoints";
import type { ClusterRoleBinding, ClusterRoleBindingApi, ClusterRoleBindingData } from "../../../../common/k8s-api/endpoints";
import type { Subject } from "../../../../common/k8s-api/endpoints/types/subject";
import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
import { autoBind, HashSet } from "../../../utils";
import { hashClusterRoleBindingSubject } from "./hashers";
export class ClusterRoleBindingsStore extends KubeObjectStore<ClusterRoleBinding> {
api = clusterRoleBindingApi;
constructor() {
super();
autoBind(this);
}
import { HashSet } from "../../../utils";
import { hashSubject } from "../hashers";
export class ClusterRoleBindingStore extends KubeObjectStore<ClusterRoleBinding, ClusterRoleBindingApi, ClusterRoleBindingData> {
protected sortItems(items: ClusterRoleBinding[]) {
return super.sortItems(items, [
clusterRoleBinding => clusterRoleBinding.kind,
@@ -25,15 +17,15 @@ export class ClusterRoleBindingsStore extends KubeObjectStore<ClusterRoleBinding
]);
}
async updateSubjects(clusterRoleBinding: ClusterRoleBinding, subjects: ClusterRoleBindingSubject[]) {
async updateSubjects(clusterRoleBinding: ClusterRoleBinding, subjects: Subject[]) {
return this.update(clusterRoleBinding, {
roleRef: clusterRoleBinding.roleRef,
subjects,
});
}
async removeSubjects(clusterRoleBinding: ClusterRoleBinding, subjectsToRemove: Iterable<ClusterRoleBindingSubject>) {
const currentSubjects = new HashSet(clusterRoleBinding.getSubjects(), hashClusterRoleBindingSubject);
async removeSubjects(clusterRoleBinding: ClusterRoleBinding, subjectsToRemove: Iterable<Subject>) {
const currentSubjects = new HashSet(clusterRoleBinding.getSubjects(), hashSubject);
for (const subject of subjectsToRemove) {
currentSubjects.delete(subject);
@@ -43,6 +35,3 @@ export class ClusterRoleBindingsStore extends KubeObjectStore<ClusterRoleBinding
}
}
export const clusterRoleBindingsStore = new ClusterRoleBindingsStore();
apiManager.registerStore(clusterRoleBindingsStore);
@@ -10,9 +10,9 @@ import React from "react";
import { KubeObjectListLayout } from "../../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { ClusterRoleBindingDialog } from "./dialog";
import { clusterRoleBindingsStore } from "./store";
import { clusterRolesStore } from "../+cluster-roles/store";
import { serviceAccountsStore } from "../+service-accounts/store";
import { clusterRoleBindingStore } from "./legacy-store";
import { clusterRoleStore } from "../+cluster-roles/legacy-store";
import { serviceAccountStore } from "../+service-accounts/legacy-store";
import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout";
import { KubeObjectAge } from "../../kube-object/age";
@@ -32,8 +32,8 @@ export class ClusterRoleBindings extends React.Component {
isConfigurable
tableId="access_cluster_role_bindings"
className="ClusterRoleBindings"
store={clusterRoleBindingsStore}
dependentStores={[clusterRolesStore, serviceAccountsStore]}
store={clusterRoleBindingStore}
dependentStores={[clusterRoleStore, serviceAccountStore]}
sortingCallbacks={{
[columnId.name]: binding => binding.getName(),
[columnId.bindings]: binding => binding.getSubjectNames(),
@@ -15,7 +15,7 @@ import { showDetails } from "../../kube-detail-params";
import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications";
import { Wizard, WizardStep } from "../../wizard";
import { clusterRolesStore } from "./store";
import { clusterRoleStore } from "./legacy-store";
export interface AddClusterRoleDialogProps extends Partial<DialogProps> {
}
@@ -45,13 +45,13 @@ export class AddClusterRoleDialog extends React.Component<AddClusterRoleDialogPr
createRole = async () => {
try {
const role = await clusterRolesStore.create({ name: this.clusterRoleName });
const role = await clusterRoleStore.create({ name: this.clusterRoleName });
showDetails(role.selfLink);
this.reset();
AddClusterRoleDialog.close();
} catch (err) {
Notifications.error(err.toString());
} catch (error) {
Notifications.checkedError(error, "Unknown error occured while creating the role");
}
};
@@ -76,7 +76,8 @@ export class AddClusterRoleDialog extends React.Component<AddClusterRoleDialogPr
>
<SubTitle title="ClusterRole Name" />
<Input
required autoFocus
required
autoFocus
placeholder="Name"
iconLeft="supervisor_account"
value={this.clusterRoleName}
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { asLegacyGlobalForExtensionApi } from "../../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import clusterRoleStoreInjectable from "./store.injectable";
/**
* @deprecated use `di.inject(clusterRoleStoreInjectable)` instead
*/
export const clusterRoleStore = asLegacyGlobalForExtensionApi(clusterRoleStoreInjectable);
@@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../../../../common/k8s-api/stores-apis-can-be-created.token";
import clusterRoleApiInjectable from "../../../../common/k8s-api/endpoints/cluster-role.api.injectable";
import { kubeObjectStoreInjectionToken } from "../../../../common/k8s-api/api-manager/manager.injectable";
import { ClusterRolesStore } from "./store";
const clusterRoleStoreInjectable = getInjectable({
id: "cluster-role-store",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "clusterRoleStore is only available in certain environments");
const api = di.inject(clusterRoleApiInjectable);
return new ClusterRolesStore(api);
},
injectionToken: kubeObjectStoreInjectionToken,
});
export default clusterRoleStoreInjectable;
@@ -2,20 +2,10 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { apiManager } from "../../../../common/k8s-api/api-manager";
import type { ClusterRole } from "../../../../common/k8s-api/endpoints";
import { clusterRoleApi } from "../../../../common/k8s-api/endpoints";
import type { ClusterRole, ClusterRoleApi, ClusterRoleData } from "../../../../common/k8s-api/endpoints";
import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
import { autoBind } from "../../../utils";
export class ClusterRolesStore extends KubeObjectStore<ClusterRole> {
api = clusterRoleApi;
constructor() {
super();
autoBind(this);
}
export class ClusterRolesStore extends KubeObjectStore<ClusterRole, ClusterRoleApi, ClusterRoleData> {
protected sortItems(items: ClusterRole[]) {
return super.sortItems(items, [
clusterRole => clusterRole.kind,
@@ -23,7 +13,3 @@ export class ClusterRolesStore extends KubeObjectStore<ClusterRole> {
]);
}
}
export const clusterRolesStore = new ClusterRolesStore();
apiManager.registerStore(clusterRolesStore);
@@ -10,7 +10,7 @@ import React from "react";
import { KubeObjectListLayout } from "../../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { AddClusterRoleDialog } from "./add-dialog";
import { clusterRolesStore } from "./store";
import { clusterRoleStore } from "./legacy-store";
import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout";
import { KubeObjectAge } from "../../kube-object/age";
@@ -29,7 +29,7 @@ export class ClusterRoles extends React.Component {
isConfigurable
tableId="access_cluster_roles"
className="ClusterRoles"
store={clusterRolesStore}
store={clusterRoleStore}
sortingCallbacks={{
[columnId.name]: clusterRole => clusterRole.getName(),
[columnId.age]: clusterRole => -clusterRole.getCreationTimestamp(),
@@ -5,16 +5,15 @@
import userEvent from "@testing-library/user-event";
import React from "react";
import { clusterRolesStore } from "../../+cluster-roles/store";
import { ClusterRole } from "../../../../../common/k8s-api/endpoints";
import { RoleBindingDialog } from "../dialog";
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
import type { DiRender } from "../../../test-utils/renderFor";
import { renderFor } from "../../../test-utils/renderFor";
import directoryForUserDataInjectable
from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
jest.mock("../../+cluster-roles/store");
import directoryForUserDataInjectable from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import clusterRoleStoreInjectable from "../../+cluster-roles/store.injectable";
import storesAndApisCanBeCreatedInjectable from "../../../../stores-apis-can-be-created.injectable";
import ipcRendererInjectable from "../../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
describe("RoleBindingDialog tests", () => {
let render: DiRender;
@@ -22,21 +21,31 @@ describe("RoleBindingDialog tests", () => {
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(storesAndApisCanBeCreatedInjectable, () => true);
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(ipcRendererInjectable, () => ({
on: jest.fn(),
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
await di.runSetups();
render = renderFor(di);
(clusterRolesStore as any).items = [new ClusterRole({
apiVersion: "rbac.authorization.k8s.io/v1",
kind: "ClusterRole",
metadata: {
name: "foobar",
resourceVersion: "1",
uid: "1",
},
})];
const store = di.inject(clusterRoleStoreInjectable);
store.items.replace([
new ClusterRole({
apiVersion: "rbac.authorization.k8s.io/v1",
kind: "ClusterRole",
metadata: {
name: "foobar",
resourceVersion: "1",
uid: "1",
selfLink: "/apis/rbac.authorization.k8s.io/v1/clusterroles/foobar",
},
}),
]);
});
afterEach(() => {
@@ -8,7 +8,7 @@ import "./details.scss";
import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import type { RoleBinding, RoleBindingSubject } from "../../../../common/k8s-api/endpoints";
import type { RoleBinding } from "../../../../common/k8s-api/endpoints";
import { prevDefault } from "../../../utils";
import { AddRemoveButtons } from "../../add-remove-buttons";
import { DrawerTitle } from "../../drawer";
@@ -16,9 +16,9 @@ import type { KubeObjectDetailsProps } from "../../kube-object-details";
import { KubeObjectMeta } from "../../kube-object-meta";
import { Table, TableCell, TableHead, TableRow } from "../../table";
import { RoleBindingDialog } from "./dialog";
import { roleBindingsStore } from "./store";
import { roleBindingStore } from "./legacy-store";
import { ObservableHashSet } from "../../../../common/utils/hash-set";
import { hashRoleBindingSubject } from "./hashers";
import { hashSubject } from "../hashers";
import type { OpenConfirmDialog } from "../../confirm-dialog/open.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import openConfirmDialogInjectable from "../../confirm-dialog/open.injectable";
@@ -32,7 +32,7 @@ interface Dependencies {
@observer
class NonInjectedRoleBindingDetails extends React.Component<RoleBindingDetailsProps & Dependencies> {
selectedSubjects = new ObservableHashSet<RoleBindingSubject>([], hashRoleBindingSubject);
private readonly selectedSubjects = new ObservableHashSet([], hashSubject);
async componentDidMount() {
disposeOnUnmount(this, [
@@ -47,10 +47,14 @@ class NonInjectedRoleBindingDetails extends React.Component<RoleBindingDetailsPr
const { selectedSubjects } = this;
openConfirmDialog({
ok: () => roleBindingsStore.removeSubjects(roleBinding, selectedSubjects.toJSON()),
ok: () => roleBindingStore.removeSubjects(roleBinding, selectedSubjects.toJSON()),
labelOk: `Remove`,
message: (
<p>Remove selected bindings for <b>{roleBinding.getName()}</b>?</p>
<p>
Remove selected bindings for
<b>{roleBinding.getName()}</b>
?
</p>
),
});
};
@@ -116,7 +120,7 @@ class NonInjectedRoleBindingDetails extends React.Component<RoleBindingDetailsPr
<AddRemoveButtons
onAdd={() => RoleBindingDialog.open(roleBinding)}
onRemove={selectedSubjects.size ? this.removeSelectedSubjects : null}
onRemove={selectedSubjects.size ? this.removeSelectedSubjects : undefined}
addTooltip={`Edit bindings of ${roleRef.name}`}
removeTooltip={`Remove selected bindings from ${roleRef.name}`}
/>
@@ -9,10 +9,10 @@ import { computed, observable, makeObservable, action } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import { rolesStore } from "../+roles/store";
import { serviceAccountsStore } from "../+service-accounts/store";
import { roleStore } from "../+roles/legacy-store";
import { serviceAccountStore } from "../+service-accounts/legacy-store";
import { NamespaceSelect } from "../../+namespaces/namespace-select";
import type { ClusterRole, Role, RoleBinding, RoleBindingSubject, ServiceAccount } from "../../../../common/k8s-api/endpoints";
import type { ClusterRole, Role, RoleBinding, ServiceAccount } from "../../../../common/k8s-api/endpoints";
import { roleApi } from "../../../../common/k8s-api/endpoints";
import type { DialogProps } from "../../dialog";
import { Dialog } from "../../dialog";
@@ -22,12 +22,13 @@ import { showDetails } from "../../kube-detail-params";
import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications";
import type { SelectOption } from "../../select";
import { Select } from "../../select";
import { onMultiSelectFor, Select } from "../../select";
import { Wizard, WizardStep } from "../../wizard";
import { roleBindingsStore } from "./store";
import { clusterRolesStore } from "../+cluster-roles/store";
import { roleBindingStore } from "./legacy-store";
import { clusterRoleStore } from "../+cluster-roles/legacy-store";
import { Input } from "../../input";
import { ObservableHashSet, nFircate } from "../../../utils";
import type { Subject } from "../../../../common/k8s-api/endpoints/types/subject";
export interface RoleBindingDialogProps extends Partial<DialogProps> {
}
@@ -58,7 +59,7 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
RoleBindingDialog.state.data = undefined;
}
get roleBinding(): RoleBinding {
get roleBinding() {
return RoleBindingDialog.state.data;
}
@@ -66,26 +67,26 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
return !!this.roleBinding;
}
@observable.ref selectedRoleRef: Role | ClusterRole | undefined = undefined;
@observable.ref selectedRoleRef: Role | ClusterRole | null | undefined = null;
@observable bindingName = "";
@observable bindingNamespace = "";
@observable bindingNamespace: string | null = null;
selectedAccounts = new ObservableHashSet<ServiceAccount>([], sa => sa.metadata.uid);
selectedUsers = observable.set<string>([]);
selectedGroups = observable.set<string>([]);
@computed get selectedBindings(): RoleBindingSubject[] {
const serviceAccounts = Array.from(this.selectedAccounts, sa => ({
@computed get selectedBindings(): Subject[] {
const serviceAccounts: Subject[] = Array.from(this.selectedAccounts, sa => ({
name: sa.getName(),
kind: "ServiceAccount" as const,
kind: "ServiceAccount",
namespace: sa.getNs(),
}));
const users = Array.from(this.selectedUsers, user => ({
const users: Subject[] = Array.from(this.selectedUsers, user => ({
name: user,
kind: "User" as const,
kind: "User",
}));
const groups = Array.from(this.selectedGroups, group => ({
const groups: Subject[] = Array.from(this.selectedGroups, group => ({
name: group,
kind: "Group" as const,
kind: "Group",
}));
return [
@@ -96,27 +97,25 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
}
@computed get roleRefOptions(): SelectOption<Role | ClusterRole>[] {
const roles = rolesStore.items
.filter(role => role.getNs() === this.bindingNamespace)
.map(value => ({ value, label: value.getName() }));
const clusterRoles = clusterRolesStore.items
.map(value => ({ value, label: value.getName() }));
const roles = roleStore.items
.filter(role => role.getNs() === this.bindingNamespace);
const clusterRoles = clusterRoleStore.items;
return [
...roles,
...clusterRoles,
];
}
@computed get serviceAccountOptions(): SelectOption<ServiceAccount>[] {
return serviceAccountsStore.items.map(account => ({
value: account,
label: `${account.getName()} (${account.getNs()})`,
].map(r => ({
value: r,
label: r.getName(),
}));
}
@computed get selectedServiceAccountOptions(): SelectOption<ServiceAccount>[] {
return this.serviceAccountOptions.filter(({ value }) => this.selectedAccounts.has(value));
@computed get serviceAccountOptions(): SelectOption<ServiceAccount>[] {
return serviceAccountStore.items.map(serviceAccount => ({
value: serviceAccount,
label: `${serviceAccount.getName()} (${serviceAccount.getNs()})`,
isSelected: this.selectedAccounts.has(serviceAccount),
}));
}
onOpen = action(() => {
@@ -126,9 +125,13 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
return this.reset();
}
this.selectedRoleRef = (binding.roleRef.kind === roleApi.kind ? rolesStore : clusterRolesStore)
.items
.find(item => item.getName() === binding.roleRef.name);
const findByRoleRefName = (item: Role | ClusterRole) => item.getName() === binding.roleRef.name;
this.selectedRoleRef = (
binding.roleRef.kind === roleApi.kind
? roleStore.items.find(findByRoleRefName)
: clusterRoleStore.items.find(findByRoleRefName)
);
this.bindingName = binding.getName();
this.bindingNamespace = binding.getNs();
@@ -137,7 +140,7 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
const accountNames = new Set(saSubjects.map(acc => acc.name));
this.selectedAccounts.replace(
serviceAccountsStore.items
serviceAccountStore.items
.filter(sa => accountNames.has(sa.getName())),
);
this.selectedUsers.replace(uSubjects.map(user => user.name));
@@ -145,7 +148,7 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
});
reset = action(() => {
this.selectedRoleRef = undefined;
this.selectedRoleRef = null;
this.bindingName = "";
this.bindingNamespace = "";
this.selectedAccounts.clear();
@@ -154,12 +157,19 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
});
createBindings = async () => {
const { selectedRoleRef, bindingNamespace: namespace, selectedBindings } = this;
const { selectedRoleRef, bindingNamespace, selectedBindings, roleBinding, bindingName } = this;
if (!selectedRoleRef || !roleBinding || !bindingNamespace || !bindingName) {
return;
}
try {
const roleBinding = this.isEditing
? await roleBindingsStore.updateSubjects(this.roleBinding, selectedBindings)
: await roleBindingsStore.create({ name: this.bindingName, namespace }, {
const newRoleBinding = this.isEditing
? await roleBindingStore.updateSubjects(roleBinding, selectedBindings)
: await roleBindingStore.create({
name: bindingName,
namespace: bindingNamespace,
}, {
subjects: selectedBindings,
roleRef: {
name: selectedRoleRef.getName(),
@@ -167,10 +177,10 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
},
});
showDetails(roleBinding.selfLink);
showDetails(newRoleBinding.selfLink);
RoleBindingDialog.close();
} catch (err) {
Notifications.error(err);
Notifications.checkedError(err, `Unknown error occured while ${this.isEditing ? "editing" : "creating"} role bindings.`);
}
};
@@ -184,7 +194,7 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
isDisabled={this.isEditing}
value={this.bindingNamespace}
autoFocus={!this.isEditing}
onChange={({ value }) => this.bindingNamespace = value}
onChange={opt => this.bindingNamespace = opt?.value ?? null}
/>
<SubTitle title="Role Reference" />
@@ -195,12 +205,12 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
isDisabled={this.isEditing}
options={this.roleRefOptions}
value={this.selectedRoleRef}
onChange={({ value }) => {
if (!this.selectedRoleRef || this.bindingName === this.selectedRoleRef.getName()) {
this.bindingName = value.getName();
}
onChange={option => {
this.selectedRoleRef = option?.value;
this.selectedRoleRef = value;
if (!this.selectedRoleRef || this.bindingName === this.selectedRoleRef.getName()) {
this.bindingName = option?.value.getName() ?? "";
}
}}
/>
@@ -235,19 +245,14 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
isMulti
themeName="light"
placeholder="Select service accounts ..."
autoConvertOptions={false}
options={this.serviceAccountOptions}
value={this.selectedServiceAccountOptions}
formatOptionLabel={({ value }: SelectOption<ServiceAccount>) => (
<><Icon small material="account_box" /> {value.getName()} ({value.getNs()})</>
formatOptionLabel={option => (
<>
<Icon small material="account_box" />
{` ${option.label}`}
</>
)}
onChange={(selected: SelectOption<ServiceAccount>[] | null) => {
if (selected) {
this.selectedAccounts.replace(selected.map(opt => opt.value));
} else {
this.selectedAccounts.clear();
}
}}
onChange={onMultiSelectFor(this.selectedAccounts)}
maxMenuHeight={200}
/>
</>
@@ -269,7 +274,11 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
onOpen={this.onOpen}
>
<Wizard
header={<h5>{action} RoleBinding</h5>}
header={(
<h5>
{`${action} RoleBinding`}
</h5>
)}
done={RoleBindingDialog.close}
>
<WizardStep
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { asLegacyGlobalForExtensionApi } from "../../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import roleBindingStoreInjectable from "./store.injectable";
/**
* @deprecated use `di.inject(roleBindingStoreInjectable)` instead
*/
export const roleBindingStore = asLegacyGlobalForExtensionApi(roleBindingStoreInjectable);
@@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { kubeObjectStoreInjectionToken } from "../../../../common/k8s-api/api-manager/manager.injectable";
import roleBindingApiInjectable from "../../../../common/k8s-api/endpoints/role-binding.api.injectable";
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable";
import { RoleBindingStore } from "./store";
const roleBindingStoreInjectable = getInjectable({
id: "role-binding-store",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectable), "roleBindingStore is only available in certain environments");
const api = di.inject(roleBindingApiInjectable);
return new RoleBindingStore(api);
},
injectionToken: kubeObjectStoreInjectionToken,
});
export default roleBindingStoreInjectable;
@@ -3,16 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { apiManager } from "../../../../common/k8s-api/api-manager";
import type { RoleBinding, RoleBindingSubject } from "../../../../common/k8s-api/endpoints";
import { roleBindingApi } from "../../../../common/k8s-api/endpoints";
import type { RoleBinding, RoleBindingApi, RoleBindingData } from "../../../../common/k8s-api/endpoints";
import type { Subject } from "../../../../common/k8s-api/endpoints/types/subject";
import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
import { HashSet } from "../../../utils";
import { hashRoleBindingSubject } from "./hashers";
export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
api = roleBindingApi;
import { hashSubject } from "../hashers";
export class RoleBindingStore extends KubeObjectStore<RoleBinding, RoleBindingApi, RoleBindingData> {
protected sortItems(items: RoleBinding[]) {
return super.sortItems(items, [
roleBinding => roleBinding.kind,
@@ -20,19 +17,15 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
]);
}
protected async createItem(params: { name: string; namespace: string }, data?: Partial<RoleBinding>) {
return roleBindingApi.create(params, data);
}
async updateSubjects(roleBinding: RoleBinding, subjects: RoleBindingSubject[]) {
async updateSubjects(roleBinding: RoleBinding, subjects: Subject[]) {
return this.update(roleBinding, {
roleRef: roleBinding.roleRef,
subjects,
});
}
async removeSubjects(roleBinding: RoleBinding, subjectsToRemove: Iterable<RoleBindingSubject>) {
const currentSubjects = new HashSet(roleBinding.getSubjects(), hashRoleBindingSubject);
async removeSubjects(roleBinding: RoleBinding, subjectsToRemove: Iterable<Subject>) {
const currentSubjects = new HashSet(roleBinding.getSubjects(), hashSubject);
for (const subject of subjectsToRemove) {
currentSubjects.delete(subject);
@@ -41,7 +34,3 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
return this.updateSubjects(roleBinding, currentSubjects.toJSON());
}
}
export const roleBindingsStore = new RoleBindingsStore();
apiManager.registerStore(roleBindingsStore);
@@ -9,10 +9,10 @@ import React from "react";
import { KubeObjectListLayout } from "../../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { RoleBindingDialog } from "./dialog";
import { roleBindingsStore } from "./store";
import { rolesStore } from "../+roles/store";
import { clusterRolesStore } from "../+cluster-roles/store";
import { serviceAccountsStore } from "../+service-accounts/store";
import { roleBindingStore } from "./legacy-store";
import { roleStore } from "../+roles/legacy-store";
import { clusterRoleStore } from "../+cluster-roles/legacy-store";
import { serviceAccountStore } from "../+service-accounts/legacy-store";
import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout";
import { KubeObjectAge } from "../../kube-object/age";
@@ -32,8 +32,8 @@ export class RoleBindings extends React.Component {
isConfigurable
tableId="access_role_bindings"
className="RoleBindings"
store={roleBindingsStore}
dependentStores={[rolesStore, clusterRolesStore, serviceAccountsStore]}
store={roleBindingStore}
dependentStores={[roleStore, clusterRoleStore, serviceAccountStore]}
sortingCallbacks={{
[columnId.name]: binding => binding.getName(),
[columnId.namespace]: binding => binding.getNs(),
@@ -17,7 +17,7 @@ import { showDetails } from "../../kube-detail-params";
import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications";
import { Wizard, WizardStep } from "../../wizard";
import { rolesStore } from "./store";
import { roleStore } from "./legacy-store";
export interface AddRoleDialogProps extends Partial<DialogProps> {
}
@@ -49,13 +49,13 @@ export class AddRoleDialog extends React.Component<AddRoleDialogProps> {
createRole = async () => {
try {
const role = await rolesStore.create({ name: this.roleName, namespace: this.namespace });
const role = await roleStore.create({ name: this.roleName, namespace: this.namespace });
showDetails(role.selfLink);
this.reset();
AddRoleDialog.close();
} catch (err) {
Notifications.error(err.toString());
Notifications.checkedError(err, "Unknown error occured while creating role");
}
};
@@ -78,7 +78,8 @@ export class AddRoleDialog extends React.Component<AddRoleDialogProps> {
>
<SubTitle title="Role Name" />
<Input
required autoFocus
required
autoFocus
placeholder="Name"
iconLeft="supervisor_account"
value={this.roleName}
@@ -89,7 +90,7 @@ export class AddRoleDialog extends React.Component<AddRoleDialogProps> {
id="add-dialog-namespace-select-input"
themeName="light"
value={this.namespace}
onChange={({ value }) => this.namespace = value}
onChange={option => this.namespace = option?.value ?? "default"}
/>
</WizardStep>
</Wizard>
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { asLegacyGlobalForExtensionApi } from "../../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import roleStoreInjectable from "./store.injectable";
/**
* @deprecated use `di.inject(roleStoreInjectable)` instead
*/
export const roleStore = asLegacyGlobalForExtensionApi(roleStoreInjectable);
@@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import roleApiInjectable from "../../../../common/k8s-api/endpoints/role.api.injectable";
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable";
import { kubeObjectStoreInjectionToken } from "../../../../common/k8s-api/api-manager/manager.injectable";
import { RoleStore } from "./store";
const roleStoreInjectable = getInjectable({
id: "role-store",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectable), "roleStore is only available in certain environments");
const api = di.inject(roleApiInjectable);
return new RoleStore(api);
},
injectionToken: kubeObjectStoreInjectionToken,
});
export default roleStoreInjectable;
@@ -2,32 +2,14 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { apiManager } from "../../../../common/k8s-api/api-manager";
import type { Role } from "../../../../common/k8s-api/endpoints";
import { roleApi } from "../../../../common/k8s-api/endpoints";
import type { Role, RoleApi, RoleData } from "../../../../common/k8s-api/endpoints";
import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
import { autoBind } from "../../../utils";
export class RolesStore extends KubeObjectStore<Role> {
api = roleApi;
constructor() {
super();
autoBind(this);
}
export class RoleStore extends KubeObjectStore<Role, RoleApi, RoleData> {
protected sortItems(items: Role[]) {
return super.sortItems(items, [
role => role.kind,
role => role.getName(),
]);
}
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<Role>) {
return roleApi.create(params, data);
}
}
export const rolesStore = new RolesStore();
apiManager.registerStore(rolesStore);
@@ -10,7 +10,7 @@ import React from "react";
import { KubeObjectListLayout } from "../../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { AddRoleDialog } from "./add-dialog";
import { rolesStore } from "./store";
import { roleStore } from "./legacy-store";
import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout";
import { KubeObjectAge } from "../../kube-object/age";
@@ -29,7 +29,7 @@ export class Roles extends React.Component {
isConfigurable
tableId="access_roles"
className="Roles"
store={rolesStore}
store={roleStore}
sortingCallbacks={{
[columnId.name]: role => role.getName(),
[columnId.namespace]: role => role.getNs(),
@@ -18,7 +18,7 @@ import { showDetails } from "../../kube-detail-params";
import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications";
import { Wizard, WizardStep } from "../../wizard";
import { serviceAccountsStore } from "./store";
import { serviceAccountStore } from "./legacy-store";
export interface CreateServiceAccountDialogProps extends Partial<DialogProps> {
}
@@ -47,13 +47,13 @@ export class CreateServiceAccountDialog extends React.Component<CreateServiceAcc
const { name, namespace } = this;
try {
const serviceAccount = await serviceAccountsStore.create({ namespace, name });
const serviceAccount = await serviceAccountStore.create({ namespace, name });
this.name = "";
showDetails(serviceAccount.selfLink);
CreateServiceAccountDialog.close();
} catch (err) {
Notifications.error(err);
Notifications.checkedError(err, "Unknown error occured while creating service account");
}
};
@@ -78,14 +78,15 @@ export class CreateServiceAccountDialog extends React.Component<CreateServiceAcc
placeholder="Enter a name"
trim
validators={systemName}
value={name} onChange={v => this.name = v.toLowerCase()}
value={name}
onChange={v => this.name = v.toLowerCase()}
/>
<SubTitle title="Namespace" />
<NamespaceSelect
id="create-dialog-namespace-select-input"
themeName="light"
value={namespace}
onChange={({ value }) => this.namespace = value}
onChange={option => this.namespace = option?.value ?? "default"}
/>
</WizardStep>
</Wizard>
@@ -5,14 +5,13 @@
import "./details.scss";
import { autorun, observable, makeObservable } from "mobx";
import { autorun, observable, runInAction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import { Link } from "react-router-dom";
import { secretsStore } from "../../+config-secrets/secrets.store";
import type { ServiceAccount } from "../../../../common/k8s-api/endpoints";
import { Secret, SecretType } from "../../../../common/k8s-api/endpoints";
import { secretStore } from "../../+config-secrets/legacy-store";
import type { Secret, ServiceAccount } from "../../../../common/k8s-api/endpoints";
import { DrawerItem, DrawerTitle } from "../../drawer";
import { Icon } from "../../icon";
import type { KubeObjectDetailsProps } from "../../kube-object-details";
@@ -24,41 +23,50 @@ import { getDetailsUrl } from "../../kube-detail-params";
export interface ServiceAccountsDetailsProps extends KubeObjectDetailsProps<ServiceAccount> {
}
const defensiveLoadSecretIn = (namespace: string) => (
({ name }: { name: string }) => (
secretStore.load({ name, namespace })
.catch(() => name)
)
);
@observer
export class ServiceAccountsDetails extends React.Component<ServiceAccountsDetailsProps> {
@observable secrets: Secret[];
@observable imagePullSecrets: Secret[];
readonly secrets = observable.array<Secret | string>();
readonly imagePullSecrets = observable.array<Secret | string>();
componentDidMount(): void {
disposeOnUnmount(this, [
autorun(async () => {
this.secrets = null;
this.imagePullSecrets = null;
const { object: serviceAccount } = this.props;
runInAction(() => {
this.secrets.clear();
this.imagePullSecrets.clear();
});
if (!serviceAccount) {
const { object: serviceAccount } = this.props;
const namespace = serviceAccount?.getNs();
if (!namespace) {
return;
}
const namespace = serviceAccount.getNs();
const secrets = serviceAccount.getSecrets().map(({ name }) => {
return secretsStore.load({ name, namespace });
});
this.secrets = await Promise.all(secrets);
const imagePullSecrets = serviceAccount.getImagePullSecrets().map(async ({ name }) => {
return secretsStore.load({ name, namespace }).catch(() => this.generateDummySecretObject(name));
});
const defensiveLoadSecret = defensiveLoadSecretIn(namespace);
this.imagePullSecrets = await Promise.all(imagePullSecrets);
const secretLoaders = Promise.all(serviceAccount.getSecrets().map(defensiveLoadSecret));
const imagePullSecretLoaders = Promise.all(serviceAccount.getImagePullSecrets().map(defensiveLoadSecret));
const [secrets, imagePullSecrets] = await Promise.all([
secretLoaders,
imagePullSecretLoaders,
]);
runInAction(() => {
this.secrets.replace(secrets);
this.imagePullSecrets.replace(imagePullSecrets);
});
}),
]);
}
constructor(props: ServiceAccountsDetailsProps) {
super(props);
makeObservable(this);
}
renderSecrets() {
const { secrets } = this;
@@ -66,9 +74,12 @@ export class ServiceAccountsDetails extends React.Component<ServiceAccountsDetai
return <Spinner center/>;
}
return secrets.map(secret =>
<ServiceAccountsSecret key={secret.getId()} secret={secret}/>,
);
return secrets.map(secret => (
<ServiceAccountsSecret
key={typeof secret === "string" ? secret : secret.getName()}
secret={secret}
/>
));
}
renderImagePullSecrets() {
@@ -81,14 +92,15 @@ export class ServiceAccountsDetails extends React.Component<ServiceAccountsDetai
return this.renderSecretLinks(imagePullSecrets);
}
renderSecretLinks(secrets: Secret[]) {
renderSecretLinks(secrets: (Secret | string)[]) {
return secrets.map((secret) => {
if (secret.getId() === null) {
if (typeof secret === "string") {
return (
<div key={secret.getName()}>
{secret.getName()}
<div key={secret}>
{secret}
<Icon
small material="warning"
small
material="warning"
tooltip="Secret is not found"
/>
</div>
@@ -103,27 +115,13 @@ export class ServiceAccountsDetails extends React.Component<ServiceAccountsDetai
});
}
generateDummySecretObject(name: string) {
return new Secret({
apiVersion: "v1",
kind: "Secret",
metadata: {
name,
uid: null,
selfLink: null,
resourceVersion: null,
},
type: SecretType.Opaque,
});
}
render() {
const { object: serviceAccount } = this.props;
if (!serviceAccount) {
return null;
}
const tokens = secretsStore.items.filter(secret =>
const tokens = secretStore.items.filter(secret =>
secret.getNs() == serviceAccount.getNs() &&
secret.getAnnotations().some(annot => annot == `kubernetes.io/service-account.name: ${serviceAccount.getName()}`),
);
@@ -133,16 +131,16 @@ export class ServiceAccountsDetails extends React.Component<ServiceAccountsDetai
<div className="ServiceAccountsDetails">
<KubeObjectMeta object={serviceAccount}/>
{tokens.length > 0 &&
<DrawerItem name="Tokens" className="links">
{this.renderSecretLinks(tokens)}
</DrawerItem>
}
{imagePullSecrets.length > 0 &&
<DrawerItem name="ImagePullSecrets" className="links">
{this.renderImagePullSecrets()}
</DrawerItem>
}
{tokens.length > 0 && (
<DrawerItem name="Tokens" className="links">
{this.renderSecretLinks(tokens)}
</DrawerItem>
)}
{imagePullSecrets.length > 0 && (
<DrawerItem name="ImagePullSecrets" className="links">
{this.renderImagePullSecrets()}
</DrawerItem>
)}
<DrawerTitle>Mountable secrets</DrawerTitle>
<div className="secrets">
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { asLegacyGlobalForExtensionApi } from "../../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import serviceAccountStoreInjectable from "./store.injectable";
/**
* @deprecated use `di.inject(serviceAccountStoreInjectable)` instead
*/
export const serviceAccountStore = asLegacyGlobalForExtensionApi(serviceAccountStoreInjectable);
@@ -13,20 +13,24 @@ import { prevDefault } from "../../../utils";
import { Icon } from "../../icon";
export interface ServiceAccountsSecretProps {
secret: Secret;
secret: Secret | string;
}
interface State {
showToken: boolean;
}
interface RenderRowArgs {
name: string;
value: React.ReactNode;
}
export class ServiceAccountsSecret extends React.Component<ServiceAccountsSecretProps, State> {
public state: State = {
showToken: false,
};
renderSecretValue() {
const { secret } = this.props;
renderSecretValue(secret: Secret) {
const { showToken } = this.state;
return (
@@ -35,7 +39,8 @@ export class ServiceAccountsSecret extends React.Component<ServiceAccountsSecret
<>
<span className="asterisks">{"•".repeat(16)}</span>
<Icon
small material="lock_open"
small
material="lock_open"
tooltip="Show value"
onClick={prevDefault(() => this.setState({ showToken: true }))}
/>
@@ -48,29 +53,52 @@ export class ServiceAccountsSecret extends React.Component<ServiceAccountsSecret
);
}
renderRow({ name, value }: RenderRowArgs) {
return (
<div className="secret-row">
<span className="name">{name}</span>
<span className="value">{value}</span>
</div>
);
}
render() {
const { metadata: { name, creationTimestamp }, type } = this.props.secret;
const { secret } = this.props;
return (
<div className="ServiceAccountsSecret box grow-fixed">
<div className="secret-row">
<span className="name">Name: </span>
<span className="value">{name}</span>
</div>
<div className="secret-row">
<span className="name">Value: </span>
<span className="value flex align-center">{this.renderSecretValue()}</span>
</div>
<div className="secret-row">
<span className="name">Created at: </span>
<span className="value" title={creationTimestamp}>
{moment(creationTimestamp).format("LLL")}
</span>
</div>
<div className="secret-row">
<span className="name">Type: </span>
<span className="value">{type}</span>
</div>
{this.renderRow({
name: "Name: ",
value: (
typeof secret === "string"
? secret
: secret.getName()
),
})}
{this.renderRow({
name: "Value: ",
value: (
typeof secret === "string"
? "<unknown>"
: this.renderSecretValue(secret)
),
})}
{this.renderRow({
name: "Created at: ",
value: (
typeof secret === "string" || !secret.metadata.creationTimestamp
? "<unknown>"
: moment(secret.metadata.creationTimestamp).format("LLL")
),
})}
{this.renderRow({
name: "Type: ",
value: (
typeof secret === "string"
? "<unknown>"
: secret.type
),
})}
</div>
);
}
@@ -15,7 +15,11 @@ export function ServiceAccountMenu(props: KubeObjectMenuProps<ServiceAccount>) {
return (
<MenuItem onClick={() => openServiceAccountKubeConfig(object)}>
<Icon material="insert_drive_file" tooltip="Kubeconfig File" interactive={toolbar} />
<Icon
material="insert_drive_file"
tooltip="Kubeconfig File"
interactive={toolbar}
/>
<span className="title">Kubeconfig</span>
</MenuItem>
);
@@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import serviceAccountApiInjectable from "../../../../common/k8s-api/endpoints/service-account.api.injectable";
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable";
import { kubeObjectStoreInjectionToken } from "../../../../common/k8s-api/api-manager/manager.injectable";
import { ServiceAccountStore } from "./store";
const serviceAccountStoreInjectable = getInjectable({
id: "service-account-store",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectable), "serviceAccountStore is only available in certain environments");
const api = di.inject(serviceAccountApiInjectable);
return new ServiceAccountStore(api);
},
injectionToken: kubeObjectStoreInjectionToken,
});
export default serviceAccountStoreInjectable;
@@ -3,26 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { apiManager } from "../../../../common/k8s-api/api-manager";
import type { ServiceAccount } from "../../../../common/k8s-api/endpoints";
import { serviceAccountsApi } from "../../../../common/k8s-api/endpoints";
import type { ServiceAccount, ServiceAccountApi, ServiceAccountData } from "../../../../common/k8s-api/endpoints";
import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
import { autoBind } from "../../../utils";
export class ServiceAccountsStore extends KubeObjectStore<ServiceAccount> {
api = serviceAccountsApi;
constructor() {
super();
autoBind(this);
}
export class ServiceAccountStore extends KubeObjectStore<ServiceAccount, ServiceAccountApi, ServiceAccountData> {
protected async createItem(params: { name: string; namespace?: string }) {
await super.createItem(params);
return this.api.get(params); // hackfix: load freshly created account, cause it doesn't have "secrets" field yet
}
}
export const serviceAccountsStore = new ServiceAccountsStore();
apiManager.registerStore(serviceAccountsStore);
@@ -10,7 +10,7 @@ import React from "react";
import { KubeObjectListLayout } from "../../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { CreateServiceAccountDialog } from "./create-dialog";
import { serviceAccountsStore } from "./store";
import { serviceAccountStore } from "./legacy-store";
import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout";
import { KubeObjectAge } from "../../kube-object/age";
@@ -28,7 +28,8 @@ export class ServiceAccounts extends React.Component {
<KubeObjectListLayout
isConfigurable
tableId="access_service_accounts"
className="ServiceAccounts" store={serviceAccountsStore}
className="ServiceAccounts"
store={serviceAccountStore}
sortingCallbacks={{
[columnId.name]: account => account.getName(),
[columnId.namespace]: account => account.getNs(),
@@ -4,9 +4,9 @@
*/
import { MD5 } from "crypto-js";
import type { RoleBindingSubject } from "../../../../common/k8s-api/endpoints";
import type { Subject } from "../../../common/k8s-api/endpoints/types/subject";
export function hashRoleBindingSubject(subject: RoleBindingSubject): string {
export function hashSubject(subject: Subject): string {
return MD5(JSON.stringify([
["kind", subject.kind],
["name", subject.name],