mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
fixes & refactoring
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
4a8079debc
commit
5a76c2f331
@ -7,38 +7,49 @@ export type KubeResource =
|
|||||||
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets";
|
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets";
|
||||||
|
|
||||||
export interface KubeApiResource {
|
export interface KubeApiResource {
|
||||||
|
kind: string; // resource type
|
||||||
resource: KubeResource; // valid resource name
|
resource: KubeResource; // valid resource name
|
||||||
group?: string; // api-group
|
group?: string; // api-group
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7)
|
// TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7)
|
||||||
export const apiResources: KubeApiResource[] = [
|
export const apiResources: KubeApiResource[] = [
|
||||||
{ resource: "configmaps" },
|
{ kind: "ConfigMap", resource: "configmaps" },
|
||||||
{ resource: "cronjobs", group: "batch" },
|
{ kind: "CronJob", resource: "cronjobs", group: "batch" },
|
||||||
{ resource: "customresourcedefinitions", group: "apiextensions.k8s.io" },
|
{ kind: "CustomResourceDefinition", resource: "customresourcedefinitions", group: "apiextensions.k8s.io" },
|
||||||
{ resource: "daemonsets", group: "apps" },
|
{ kind: "DaemonSet", resource: "daemonsets", group: "apps" },
|
||||||
{ resource: "deployments", group: "apps" },
|
{ kind: "Deployment", resource: "deployments", group: "apps" },
|
||||||
{ resource: "endpoints" },
|
{ kind: "Endpoint", resource: "endpoints" },
|
||||||
{ resource: "events" },
|
{ kind: "Event", resource: "events" },
|
||||||
{ resource: "horizontalpodautoscalers" },
|
{ kind: "HorizontalPodAutoscaler", resource: "horizontalpodautoscalers" },
|
||||||
{ resource: "ingresses", group: "networking.k8s.io" },
|
{ kind: "Ingress", resource: "ingresses", group: "networking.k8s.io" },
|
||||||
{ resource: "jobs", group: "batch" },
|
{ kind: "Job", resource: "jobs", group: "batch" },
|
||||||
{ resource: "namespaces" },
|
{ kind: "Namespace", resource: "namespaces" },
|
||||||
{ resource: "networkpolicies", group: "networking.k8s.io" },
|
{ kind: "NetworkPolicy", resource: "networkpolicies", group: "networking.k8s.io" },
|
||||||
{ resource: "nodes" },
|
{ kind: "Node", resource: "nodes" },
|
||||||
{ resource: "persistentvolumes" },
|
{ kind: "PersistentVolume", resource: "persistentvolumes" },
|
||||||
{ resource: "persistentvolumeclaims" },
|
{ kind: "PersistentVolumeClaim", resource: "persistentvolumeclaims" },
|
||||||
{ resource: "pods" },
|
{ kind: "Pod", resource: "pods" },
|
||||||
{ resource: "poddisruptionbudgets" },
|
{ kind: "PodDisruptionBudget", resource: "poddisruptionbudgets" },
|
||||||
{ resource: "podsecuritypolicies" },
|
{ kind: "PodSecurityPolicy", resource: "podsecuritypolicies" },
|
||||||
{ resource: "resourcequotas" },
|
{ kind: "ResourceQuota", resource: "resourcequotas" },
|
||||||
{ resource: "replicasets", group: "apps" },
|
{ kind: "ReplicaSet", resource: "replicasets", group: "apps" },
|
||||||
{ resource: "secrets" },
|
{ kind: "Secret", resource: "secrets" },
|
||||||
{ resource: "services" },
|
{ kind: "Service", resource: "services" },
|
||||||
{ resource: "statefulsets", group: "apps" },
|
{ kind: "StatefulSet", resource: "statefulsets", group: "apps" },
|
||||||
{ resource: "storageclasses", group: "storage.k8s.io" },
|
{ kind: "StorageClass", resource: "storageclasses", group: "storage.k8s.io" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function isAllowedResourceType(kind: string): boolean {
|
||||||
|
const apiResource = apiResources.find(resource => resource.kind === kind);
|
||||||
|
|
||||||
|
if (apiResource) {
|
||||||
|
return getHostedCluster().allowedResources.includes(apiResource.resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // allowed by default for other resources
|
||||||
|
}
|
||||||
|
|
||||||
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
|
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
|
||||||
if (!Array.isArray(resources)) {
|
if (!Array.isArray(resources)) {
|
||||||
resources = [resources];
|
resources = [resources];
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { debounce } from "lodash";
|
||||||
import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
|
import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
|
||||||
import { autobind, createStorage } from "../../utils";
|
import { autobind, createStorage } from "../../utils";
|
||||||
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
||||||
@ -7,14 +8,14 @@ import { apiManager } from "../../api/api-manager";
|
|||||||
import { isAllowedResource } from "../../../common/rbac";
|
import { isAllowedResource } from "../../../common/rbac";
|
||||||
import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
|
import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
|
||||||
|
|
||||||
const storage = createStorage<string[]>("context_namespaces", []);
|
const storage = createStorage<string[]>("context_namespaces");
|
||||||
|
|
||||||
export const namespaceUrlParam = createPageParam<string[]>({
|
export const namespaceUrlParam = createPageParam<string[]>({
|
||||||
name: "namespaces",
|
name: "namespaces",
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
multiValues: true,
|
multiValues: true,
|
||||||
get defaultValue() {
|
get defaultValue() {
|
||||||
return storage.get(); // initial namespaces coming from URL or local-storage (default)
|
return storage.get() ?? []; // initial namespaces coming from URL or local-storage (default)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -51,9 +52,9 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
await getHostedCluster().whenReady; // wait for cluster-state from main
|
await getHostedCluster().whenReady; // wait for cluster-state from main
|
||||||
this.isReady = true;
|
this.isReady = true;
|
||||||
|
|
||||||
this.setContext(this.initNamespaces);
|
this.setContext(this.initialNamespaces);
|
||||||
this.onSelectedNamespacesChange();
|
this.autoLoadAllowedNamespaces();
|
||||||
this.onAllowedNamespacesChange();
|
this.autoUpdateUrlAndLocalStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
|
public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
|
||||||
@ -63,7 +64,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSelectedNamespacesChange(): IReactionDisposer {
|
private autoUpdateUrlAndLocalStorage(): IReactionDisposer {
|
||||||
return this.onContextChange(namespaces => {
|
return this.onContextChange(namespaces => {
|
||||||
storage.set(namespaces); // save to local-storage
|
storage.set(namespaces); // save to local-storage
|
||||||
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
|
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
|
||||||
@ -73,7 +74,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAllowedNamespacesChange(): IReactionDisposer {
|
private autoLoadAllowedNamespaces(): IReactionDisposer {
|
||||||
return reaction(() => this.allowedNamespaces, () => this.loadAll(), {
|
return reaction(() => this.allowedNamespaces, () => this.loadAll(), {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
equals: comparer.identity,
|
equals: comparer.identity,
|
||||||
@ -84,28 +85,19 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
return toJS(getHostedCluster().allowedNamespaces);
|
return toJS(getHostedCluster().allowedNamespaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
get initNamespaces() {
|
private get initialNamespaces(): string[] {
|
||||||
const allowedNamespaces = new Set(this.allowedNamespaces);
|
const allowed = new Set(this.allowedNamespaces);
|
||||||
const lastUsedNamespaces = new Set(storage.get());
|
const prevSelected = storage.get();
|
||||||
|
|
||||||
// remove previously saved, but currently disallowed namespaces
|
if (Array.isArray(prevSelected)) {
|
||||||
lastUsedNamespaces.forEach(namespace => {
|
return prevSelected.filter(namespace => allowed.has(namespace));
|
||||||
if (!allowedNamespaces.has(namespace)) {
|
|
||||||
lastUsedNamespaces.delete(namespace);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// return previously saved and currently allowed namespaces
|
|
||||||
if (lastUsedNamespaces.size) {
|
|
||||||
return Array.from(lastUsedNamespaces);
|
|
||||||
}
|
|
||||||
// otherwise select "default" or first allowed namespace
|
// otherwise select "default" or first allowed namespace
|
||||||
else {
|
if (allowed.has("default")) {
|
||||||
if (allowedNamespaces.has("default")) {
|
|
||||||
return ["default"];
|
return ["default"];
|
||||||
} else if (allowedNamespaces.size) {
|
} else if (allowed.size) {
|
||||||
return [Array.from(allowedNamespaces)[0]];
|
return [Array.from(allowed)[0]];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
@ -132,17 +124,18 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
return super.subscribe(apis);
|
return super.subscribe(apis);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadAll() {
|
// prevent multiple loading from different sources (e.g. items-list-layout, namespace-select)
|
||||||
return super.loadAll({
|
private loadAllLazy = debounce(() => {
|
||||||
|
super.loadAll({
|
||||||
namespaces: this.allowedNamespaces,
|
namespaces: this.allowedNamespaces,
|
||||||
});
|
});
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
async loadAll() {
|
||||||
|
this.loadAllLazy();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams) {
|
protected async loadItems({ namespaces }: KubeObjectStoreLoadingParams) {
|
||||||
if (isAdmin) {
|
|
||||||
return this.api.list();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAllowedResource("namespaces")) {
|
if (!isAllowedResource("namespaces")) {
|
||||||
return namespaces.map(getDummyNamespace);
|
return namespaces.map(getDummyNamespace);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,10 +26,10 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
|||||||
return clusterRoleBindingApi.get(params);
|
return clusterRoleBindingApi.get(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams): Promise<RoleBinding[]> {
|
protected async loadItems(params: KubeObjectStoreLoadingParams): Promise<RoleBinding[]> {
|
||||||
const items = await Promise.all([
|
const items = await Promise.all([
|
||||||
super.loadItems({ isAdmin, namespaces, api: clusterRoleBindingApi }),
|
super.loadItems({ ...params, api: clusterRoleBindingApi }),
|
||||||
super.loadItems({ isAdmin, namespaces, api: roleBindingApi }),
|
super.loadItems({ ...params, api: roleBindingApi }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return items.flat();
|
return items.flat();
|
||||||
|
|||||||
@ -24,10 +24,10 @@ export class RolesStore extends KubeObjectStore<Role> {
|
|||||||
return clusterRoleApi.get(params);
|
return clusterRoleApi.get(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams): Promise<Role[]> {
|
protected async loadItems(params: KubeObjectStoreLoadingParams): Promise<Role[]> {
|
||||||
const items = await Promise.all([
|
const items = await Promise.all([
|
||||||
super.loadItems({ isAdmin, namespaces, api: clusterRoleApi }),
|
super.loadItems({ ...params, api: clusterRoleApi }),
|
||||||
super.loadItems({ isAdmin, namespaces, api: roleApi }),
|
super.loadItems({ ...params, api: roleApi }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return items.flat();
|
return items.flat();
|
||||||
|
|||||||
@ -48,7 +48,6 @@ export class WorkloadsOverview extends React.Component<Props> {
|
|||||||
if (this.isUnmounting) break;
|
if (this.isUnmounting) break;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
store.reset();
|
|
||||||
await store.loadAll();
|
await store.loadAll();
|
||||||
unsubscribeMap.get(store)?.(); // unsubscribe previous watcher
|
unsubscribeMap.get(store)?.(); // unsubscribe previous watcher
|
||||||
unsubscribeMap.set(store, store.subscribe());
|
unsubscribeMap.set(store, store.subscribe());
|
||||||
|
|||||||
@ -112,11 +112,11 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
|
this.loadStores();
|
||||||
|
|
||||||
if (!this.props.isClusterScoped) {
|
if (!this.props.isClusterScoped) {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
namespaceStore.onContextChange(() => this.loadStores(), {
|
namespaceStore.onContextChange(() => this.loadStores())
|
||||||
fireImmediately: true,
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,7 +148,6 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
store.reset();
|
|
||||||
await store.loadAll();
|
await store.loadAll();
|
||||||
this.watchDisposers.push(store.subscribe());
|
this.watchDisposers.push(store.subscribe());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -195,9 +194,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@computed get isReady() {
|
@computed get isReady() {
|
||||||
const { isReady, store } = this.props;
|
return this.props.isReady ?? this.props.store.isLoaded;
|
||||||
|
|
||||||
return typeof isReady == "boolean" ? isReady : store.isLoaded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get filters() {
|
@computed get filters() {
|
||||||
@ -330,12 +327,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderNoItems() {
|
renderNoItems() {
|
||||||
const { allItems, items, filters } = this;
|
if (this.filters.length > 0) {
|
||||||
const allItemsCount = allItems.length;
|
|
||||||
const itemsCount = items.length;
|
|
||||||
const isFiltered = filters.length > 0 && allItemsCount > itemsCount;
|
|
||||||
|
|
||||||
if (isFiltered) {
|
|
||||||
return (
|
return (
|
||||||
<NoItems>
|
<NoItems>
|
||||||
No items found.
|
No items found.
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export interface ItemObject {
|
|||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export abstract class ItemStore<T extends ItemObject = ItemObject> {
|
export abstract class ItemStore<T extends ItemObject = ItemObject> {
|
||||||
abstract loadAll(...args: any[]): Promise<any>;
|
abstract loadAll(...args: any[]): Promise<void>;
|
||||||
|
|
||||||
protected defaultSorting = (item: T) => item.getName();
|
protected defaultSorting = (item: T) => item.getName();
|
||||||
|
|
||||||
|
|||||||
@ -6,10 +6,9 @@ import { ItemStore } from "./item.store";
|
|||||||
import { apiManager } from "./api/api-manager";
|
import { apiManager } from "./api/api-manager";
|
||||||
import { IKubeApiQueryParams, KubeApi } from "./api/kube-api";
|
import { IKubeApiQueryParams, KubeApi } from "./api/kube-api";
|
||||||
import { KubeJsonApiData } from "./api/kube-json-api";
|
import { KubeJsonApiData } from "./api/kube-json-api";
|
||||||
import { getHostedCluster } from "../common/cluster-store";
|
import { isAllowedResourceType } from "../common/rbac";
|
||||||
|
|
||||||
export interface KubeObjectStoreLoadingParams {
|
export interface KubeObjectStoreLoadingParams {
|
||||||
isAdmin: boolean;
|
|
||||||
namespaces: string[];
|
namespaces: string[];
|
||||||
api?: KubeApi;
|
api?: KubeApi;
|
||||||
}
|
}
|
||||||
@ -77,45 +76,40 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadItems({ isAdmin, namespaces, api }: KubeObjectStoreLoadingParams): Promise<T[]> {
|
protected async loadItems({ namespaces, api }: KubeObjectStoreLoadingParams): Promise<T[]> {
|
||||||
if (!api.isNamespaced) {
|
if (isAllowedResourceType(api.kind)) {
|
||||||
if (isAdmin) return api.list({}, this.query);
|
if (api.isNamespaced) {
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise
|
return Promise
|
||||||
.all(namespaces.map(namespace => api.list({ namespace })))
|
.all(namespaces.map(namespace => api.list({ namespace })))
|
||||||
.then(items => items.flat());
|
.then(items => items.flat());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return api.list({}, this.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
protected filterItemsOnLoad(items: T[]) {
|
protected filterItemsOnLoad(items: T[]) {
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll(params: { namespaces?: string[] } = {}) {
|
async loadAll({ namespaces: contextNamespaces }: { namespaces?: string[] } = {}) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
let items: T[];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let contextNamespaces = params.namespaces;
|
if (!contextNamespaces) {
|
||||||
|
|
||||||
if (!params.namespaces) {
|
|
||||||
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
|
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
|
||||||
|
|
||||||
await namespaceStore.whenReady;
|
|
||||||
contextNamespaces = namespaceStore.getContextNamespaces();
|
contextNamespaces = namespaceStore.getContextNamespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
items = await this.loadItems({
|
let items = await this.loadItems({ namespaces: contextNamespaces, api: this.api });
|
||||||
isAdmin: getHostedCluster().isAdmin,
|
|
||||||
namespaces: contextNamespaces,
|
|
||||||
api: this.api,
|
|
||||||
});
|
|
||||||
|
|
||||||
items = this.filterItemsOnLoad(items);
|
items = this.filterItemsOnLoad(items);
|
||||||
items = this.sortItems(items);
|
items = this.sortItems(items);
|
||||||
|
|
||||||
this.items.replace(items);
|
this.items.replace(items);
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user