1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Revert "Load k8s resources only for selected namespaces (#1918)"

This reverts commit f8c111ddd8.

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2021-02-05 11:16:55 +02:00
parent 6f7cb4d568
commit 46fa5bbc11
17 changed files with 246 additions and 325 deletions

View File

@ -7,38 +7,37 @@ export type KubeResource =
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets";
export interface KubeApiResource {
kind: string; // resource type (e.g. "Namespace")
apiName: KubeResource; // valid api resource name (e.g. "namespaces")
resource: KubeResource; // valid resource name
group?: string; // api-group
}
// TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7)
export const apiResources: KubeApiResource[] = [
{ kind: "ConfigMap", apiName: "configmaps" },
{ kind: "CronJob", apiName: "cronjobs", group: "batch" },
{ kind: "CustomResourceDefinition", apiName: "customresourcedefinitions", group: "apiextensions.k8s.io" },
{ kind: "DaemonSet", apiName: "daemonsets", group: "apps" },
{ kind: "Deployment", apiName: "deployments", group: "apps" },
{ kind: "Endpoint", apiName: "endpoints" },
{ kind: "Event", apiName: "events" },
{ kind: "HorizontalPodAutoscaler", apiName: "horizontalpodautoscalers" },
{ kind: "Ingress", apiName: "ingresses", group: "networking.k8s.io" },
{ kind: "Job", apiName: "jobs", group: "batch" },
{ kind: "Namespace", apiName: "namespaces" },
{ kind: "LimitRange", apiName: "limitranges" },
{ kind: "NetworkPolicy", apiName: "networkpolicies", group: "networking.k8s.io" },
{ kind: "Node", apiName: "nodes" },
{ kind: "PersistentVolume", apiName: "persistentvolumes" },
{ kind: "PersistentVolumeClaim", apiName: "persistentvolumeclaims" },
{ kind: "Pod", apiName: "pods" },
{ kind: "PodDisruptionBudget", apiName: "poddisruptionbudgets" },
{ kind: "PodSecurityPolicy", apiName: "podsecuritypolicies" },
{ kind: "ResourceQuota", apiName: "resourcequotas" },
{ kind: "ReplicaSet", apiName: "replicasets", group: "apps" },
{ kind: "Secret", apiName: "secrets" },
{ kind: "Service", apiName: "services" },
{ kind: "StatefulSet", apiName: "statefulsets", group: "apps" },
{ kind: "StorageClass", apiName: "storageclasses", group: "storage.k8s.io" },
{ resource: "configmaps" },
{ resource: "cronjobs", group: "batch" },
{ resource: "customresourcedefinitions", group: "apiextensions.k8s.io" },
{ resource: "daemonsets", group: "apps" },
{ resource: "deployments", group: "apps" },
{ resource: "endpoints" },
{ resource: "events" },
{ resource: "horizontalpodautoscalers" },
{ resource: "ingresses", group: "networking.k8s.io" },
{ resource: "jobs", group: "batch" },
{ resource: "limitranges" },
{ resource: "namespaces" },
{ resource: "networkpolicies", group: "networking.k8s.io" },
{ resource: "nodes" },
{ resource: "persistentvolumes" },
{ resource: "persistentvolumeclaims" },
{ resource: "pods" },
{ resource: "poddisruptionbudgets" },
{ resource: "podsecuritypolicies" },
{ resource: "resourcequotas" },
{ resource: "replicasets", group: "apps" },
{ resource: "secrets" },
{ resource: "services" },
{ resource: "statefulsets", group: "apps" },
{ resource: "storageclasses", group: "storage.k8s.io" },
];
export function isAllowedResource(resources: KubeResource | KubeResource[]) {

View File

@ -190,7 +190,7 @@ export class Cluster implements ClusterModel, ClusterState {
*/
@observable metadata: ClusterMetadata = {};
/**
* List of allowed namespaces verified via K8S::SelfSubjectAccessReview api
* List of allowed namespaces
*
* @observable
*/
@ -203,7 +203,7 @@ export class Cluster implements ClusterModel, ClusterState {
*/
@observable allowedResources: string[] = [];
/**
* List of accessible namespaces provided by user in the Cluster Settings
* List of accessible namespaces
*
* @observable
*/
@ -224,7 +224,7 @@ export class Cluster implements ClusterModel, ClusterState {
* @computed
*/
@computed get name() {
return this.preferences.clusterName || this.contextName;
return this.preferences.clusterName || this.contextName;
}
/**
@ -279,8 +279,7 @@ export class Cluster implements ClusterModel, ClusterState {
* @param port port where internal auth proxy is listening
* @internal
*/
@action
async init(port: number) {
@action async init(port: number) {
try {
this.initializing = true;
this.contextHandler = new ContextHandler(this);
@ -335,8 +334,7 @@ export class Cluster implements ClusterModel, ClusterState {
* @param force force activation
* @internal
*/
@action
async activate(force = false) {
@action async activate(force = false) {
if (this.activated && !force) {
return this.pushState();
}
@ -375,8 +373,7 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
*/
@action
async reconnect() {
@action async reconnect() {
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
this.contextHandler?.stopServer();
await this.contextHandler?.ensureServer();
@ -403,8 +400,7 @@ export class Cluster implements ClusterModel, ClusterState {
* @internal
* @param opts refresh options
*/
@action
async refresh(opts: ClusterRefreshOptions = {}) {
@action async refresh(opts: ClusterRefreshOptions = {}) {
logger.info(`[CLUSTER]: refresh`, this.getMeta());
await this.whenInitialized;
await this.refreshConnectionStatus();
@ -424,8 +420,7 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
*/
@action
async refreshMetadata() {
@action async refreshMetadata() {
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
const metadata = await detectorRegistry.detectForCluster(this);
const existingMetadata = this.metadata;
@ -436,8 +431,7 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
*/
@action
async refreshConnectionStatus() {
@action async refreshConnectionStatus() {
const connectionStatus = await this.getConnectionStatus();
this.online = connectionStatus > ClusterStatus.Offline;
@ -447,8 +441,7 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
*/
@action
async refreshAllowedResources() {
@action async refreshAllowedResources() {
this.allowedNamespaces = await this.getAllowedNamespaces();
this.allowedResources = await this.getAllowedResources();
}
@ -675,7 +668,7 @@ export class Cluster implements ClusterModel, ClusterState {
for (const namespace of this.allowedNamespaces.slice(0, 10)) {
if (!this.resourceAccessStatuses.get(apiResource)) {
const result = await this.canI({
resource: apiResource.apiName,
resource: apiResource.resource,
group: apiResource.group,
verb: "list",
namespace
@ -690,19 +683,9 @@ export class Cluster implements ClusterModel, ClusterState {
return apiResources
.filter((resource) => this.resourceAccessStatuses.get(resource))
.map(apiResource => apiResource.apiName);
.map(apiResource => apiResource.resource);
} catch (error) {
return [];
}
}
isAllowedResource(kind: string): boolean {
const apiResource = apiResources.find(resource => resource.kind === kind || resource.apiName === kind);
if (apiResource) {
return this.allowedResources.includes(apiResource.apiName);
}
return true; // allowed by default for other resources
}
}

View File

@ -11,7 +11,7 @@ import { apiPrefix, isDevelopment } from "../../common/vars";
import { getHostedCluster } from "../../common/cluster-store";
export interface IKubeWatchEvent<T = any> {
type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR";
type: "ADDED" | "MODIFIED" | "DELETED";
object?: T;
}
@ -62,41 +62,27 @@ export class KubeWatchApi {
});
}
// FIXME: use POST to send apis for subscribing (list could be huge)
// TODO: try to use normal fetch res.body stream to consume watch-api updates
// https://github.com/lensapp/lens/issues/1898
protected async getQuery() {
const { namespaceStore } = await import("../components/+namespaces/namespace.store");
await namespaceStore.whenReady;
const { isAdmin } = getHostedCluster();
protected getQuery(): Partial<IKubeWatchRouteQuery> {
const { isAdmin, allowedNamespaces } = getHostedCluster();
return {
api: this.activeApis.map(api => {
if (isAdmin && !api.isNamespaced) {
return api.getWatchUrl();
}
if (isAdmin) return api.getWatchUrl();
if (api.isNamespaced) {
return namespaceStore.getContextNamespaces().map(namespace => api.getWatchUrl(namespace));
}
return [];
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace));
}).flat()
};
}
// todo: maybe switch to websocket to avoid often reconnects
@autobind()
protected async connect() {
protected connect() {
if (this.evtSource) this.disconnect(); // close previous connection
const query = await this.getQuery();
if (!this.activeApis.length || !query.api.length) {
if (!this.activeApis.length) {
return;
}
const query = this.getQuery();
const apiUrl = `${apiPrefix}/watch?${stringify(query)}`;
this.evtSource = new EventSource(apiUrl);
@ -172,10 +158,6 @@ export class KubeWatchApi {
addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) {
const listener = (evt: IKubeWatchEvent<KubeJsonApiData>) => {
if (evt.type === "ERROR") {
return; // e.g. evt.object.message == "too old resource version"
}
const { namespace, resourceVersion } = evt.object.metadata;
const api = apiManager.getApiByKind(evt.object.kind, evt.object.apiVersion);

View File

@ -5,7 +5,7 @@ import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayl
import { ItemStore } from "../../item.store";
import { Secret } from "../../api/endpoints";
import { secretsStore } from "../+config-secrets/secrets.store";
import { namespaceStore } from "../+namespaces/namespace.store";
import { getHostedCluster } from "../../../common/cluster-store";
@autobind()
export class ReleaseStore extends ItemStore<HelmRelease> {
@ -60,24 +60,31 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
@action
async loadAll() {
this.isLoading = true;
let items;
try {
const items = await this.loadItems(namespaceStore.getContextNamespaces());
const { isAdmin, allowedNamespaces } = getHostedCluster();
this.items.replace(this.sortItems(items));
this.isLoaded = true;
} catch (error) {
console.error(`Loading Helm Chart releases has failed: ${error}`);
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
} finally {
if (items) {
items = this.sortItems(items);
this.items.replace(items);
}
this.isLoaded = true;
this.isLoading = false;
}
}
async loadItems(namespaces: string[]) {
async loadItems(namespaces?: string[]) {
if (!namespaces) {
return helmReleasesApi.list();
} else {
return Promise
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
.then(items => items.flat());
}
}
async create(payload: IReleaseCreatePayload) {
const response = await helmReleasesApi.create(payload);

View File

@ -1,120 +1,53 @@
import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
import { action, comparer, observable, reaction } from "mobx";
import { autobind, createStorage } from "../../utils";
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
import { KubeObjectStore } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints";
import { createPageParam } from "../../navigation";
import { apiManager } from "../../api/api-manager";
import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
import { isAllowedResource } from "../../../common/rbac";
import { getHostedCluster } from "../../../common/cluster-store";
const storage = createStorage<string[]>("context_namespaces");
const storage = createStorage<string[]>("context_namespaces", []);
export const namespaceUrlParam = createPageParam<string[]>({
name: "namespaces",
isSystem: true,
multiValues: true,
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)
}
});
export function getDummyNamespace(name: string) {
return new Namespace({
kind: Namespace.kind,
apiVersion: "v1",
metadata: {
name,
uid: "",
resourceVersion: "",
selfLink: `/api/v1/namespaces/${name}`
}
});
}
@autobind()
export class NamespaceStore extends KubeObjectStore<Namespace> {
api = namespacesApi;
@observable contextNs = observable.array<string>();
@observable isReady = false;
whenReady = when(() => this.isReady);
contextNs = observable.array<string>();
constructor() {
super();
this.init();
}
private async init() {
await clusterStore.whenLoaded;
if (!getHostedCluster()) return;
await getHostedCluster().whenReady; // wait for cluster-state from main
private init() {
this.setContext(this.initNamespaces);
this.setContext(this.initialNamespaces);
this.autoLoadAllowedNamespaces();
this.autoUpdateUrlAndLocalStorage();
this.isReady = true;
}
public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
return reaction(() => this.contextNs.toJS(), callback, {
equals: comparer.shallow,
...opts,
});
}
private autoUpdateUrlAndLocalStorage(): IReactionDisposer {
return this.onContextChange(namespaces => {
return reaction(() => this.contextNs.toJS(), namespaces => {
storage.set(namespaces); // save to local-storage
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
}, {
fireImmediately: true,
equals: comparer.identity,
});
}
private autoLoadAllowedNamespaces(): IReactionDisposer {
return reaction(() => this.allowedNamespaces, () => this.loadAll(), {
fireImmediately: true,
equals: comparer.shallow,
});
get initNamespaces() {
return namespaceUrlParam.get();
}
get allowedNamespaces(): string[] {
return toJS(getHostedCluster().allowedNamespaces);
}
private get initialNamespaces(): string[] {
const allowed = new Set(this.allowedNamespaces);
const prevSelected = storage.get();
if (Array.isArray(prevSelected)) {
return prevSelected.filter(namespace => allowed.has(namespace));
}
// otherwise select "default" or first allowed namespace
if (allowed.has("default")) {
return ["default"];
} else if (allowed.size) {
return [Array.from(allowed)[0]];
}
return [];
}
getContextNamespaces(): string[] {
const namespaces = this.contextNs.toJS();
// show all namespaces when nothing selected
if (!namespaces.length) {
if (this.isLoaded) {
// return actual namespaces list since "allowedNamespaces" updating every 30s in cluster and thus might be stale
return this.items.map(namespace => namespace.getName());
}
return this.allowedNamespaces;
}
return namespaces;
getContextParams() {
return {
namespaces: this.contextNs.toJS(),
};
}
subscribe(apis = [this.api]) {
@ -128,18 +61,31 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
return super.subscribe(apis);
}
protected async loadItems(params: KubeObjectStoreLoadingParams) {
const { allowedNamespaces } = this;
protected async loadItems(namespaces?: string[]) {
if (!isAllowedResource("namespaces")) {
if (namespaces) return namespaces.map(this.getDummyNamespace);
let namespaces = await super.loadItems(params);
namespaces = namespaces.filter(namespace => allowedNamespaces.includes(namespace.getName()));
if (!namespaces.length && allowedNamespaces.length > 0) {
return allowedNamespaces.map(getDummyNamespace);
return [];
}
return namespaces;
if (namespaces) {
return Promise.all(namespaces.map(name => this.api.get({ name })));
} else {
return super.loadItems();
}
}
protected getDummyNamespace(name: string) {
return new Namespace({
kind: "Namespace",
apiVersion: "v1",
metadata: {
name,
uid: "",
resourceVersion: "",
selfLink: `/api/v1/namespaces/${name}`
}
});
}
@action
@ -159,6 +105,12 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
else this.contextNs.push(namespace);
}
@action
reset() {
super.reset();
this.contextNs.clear();
}
@action
async remove(item: Namespace) {
await super.remove(item);

View File

@ -1,7 +1,7 @@
import difference from "lodash/difference";
import uniqBy from "lodash/uniqBy";
import { clusterRoleBindingApi, IRoleBindingSubject, RoleBinding, roleBindingApi } from "../../api/endpoints";
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { apiManager } from "../../api/api-manager";
@ -26,13 +26,15 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
return clusterRoleBindingApi.get(params);
}
protected async loadItems(params: KubeObjectStoreLoadingParams): Promise<RoleBinding[]> {
const items = await Promise.all([
super.loadItems({ ...params, api: clusterRoleBindingApi }),
super.loadItems({ ...params, api: roleBindingApi }),
]);
return items.flat();
protected loadItems(namespaces?: string[]) {
if (namespaces) {
return Promise.all(
namespaces.map(namespace => roleBindingApi.list({ namespace }))
).then(items => items.flat());
} else {
return Promise.all([clusterRoleBindingApi.list(), roleBindingApi.list()])
.then(items => items.flat());
}
}
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<RoleBinding>) {

View File

@ -1,6 +1,6 @@
import { clusterRoleApi, Role, roleApi } from "../../api/endpoints";
import { autobind } from "../../utils";
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
import { KubeObjectStore } from "../../kube-object.store";
import { apiManager } from "../../api/api-manager";
@autobind()
@ -24,13 +24,15 @@ export class RolesStore extends KubeObjectStore<Role> {
return clusterRoleApi.get(params);
}
protected async loadItems(params: KubeObjectStoreLoadingParams): Promise<Role[]> {
const items = await Promise.all([
super.loadItems({ ...params, api: clusterRoleApi }),
super.loadItems({ ...params, api: roleApi }),
]);
return items.flat();
protected loadItems(namespaces?: string[]): Promise<Role[]> {
if (namespaces) {
return Promise.all(
namespaces.map(namespace => roleApi.list({ namespace }))
).then(items => items.flat());
} else {
return Promise.all([clusterRoleApi.list(), roleApi.list()])
.then(items => items.flat());
}
}
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<Role>) {

View File

@ -27,7 +27,7 @@ export class OverviewStatuses extends React.Component {
@autobind()
renderWorkload(resource: KubeResource): React.ReactElement {
const store = workloadStores[resource];
const items = store.getAllByNs(namespaceStore.getContextNamespaces());
const items = store.getAllByNs(namespaceStore.contextNs);
return (
<div className="workload" key={resource}>

View File

@ -17,65 +17,81 @@ import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
import { Events } from "../+events";
import { KubeObjectStore } from "../../kube-object.store";
import { isAllowedResource } from "../../../common/rbac";
import { namespaceStore } from "../+namespaces/namespace.store";
interface Props extends RouteComponentProps<IWorkloadsOverviewRouteParams> {
}
@observer
export class WorkloadsOverview extends React.Component<Props> {
@observable isLoading = false;
@observable isUnmounting = false;
async componentDidMount() {
const stores: KubeObjectStore[] = [
isAllowedResource("pods") && podsStore,
isAllowedResource("deployments") && deploymentStore,
isAllowedResource("daemonsets") && daemonSetStore,
isAllowedResource("statefulsets") && statefulSetStore,
isAllowedResource("replicasets") && replicaSetStore,
isAllowedResource("jobs") && jobStore,
isAllowedResource("cronjobs") && cronJobStore,
isAllowedResource("events") && eventStore,
].filter(Boolean);
const stores: KubeObjectStore[] = [];
const unsubscribeMap = new Map<KubeObjectStore, () => void>();
if (isAllowedResource("pods")) {
stores.push(podsStore);
}
const loadStores = async () => {
this.isLoading = true;
if (isAllowedResource("deployments")) {
stores.push(deploymentStore);
}
if (isAllowedResource("daemonsets")) {
stores.push(daemonSetStore);
}
if (isAllowedResource("statefulsets")) {
stores.push(statefulSetStore);
}
if (isAllowedResource("replicasets")) {
stores.push(replicaSetStore);
}
if (isAllowedResource("jobs")) {
stores.push(jobStore);
}
if (isAllowedResource("cronjobs")) {
stores.push(cronJobStore);
}
if (isAllowedResource("events")) {
stores.push(eventStore);
}
const unsubscribeList: Array<() => void> = [];
for (const store of stores) {
if (this.isUnmounting) break;
try {
await store.loadAll();
unsubscribeMap.get(store)?.(); // unsubscribe previous watcher
unsubscribeMap.set(store, store.subscribe());
} catch (error) {
console.error("loading store error", error);
unsubscribeList.push(store.subscribe());
}
}
this.isLoading = false;
};
namespaceStore.onContextChange(loadStores, {
fireImmediately: true,
});
await when(() => this.isUnmounting && !this.isLoading);
unsubscribeMap.forEach(dispose => dispose());
unsubscribeMap.clear();
await when(() => this.isUnmounting);
unsubscribeList.forEach(dispose => dispose());
}
componentWillUnmount() {
this.isUnmounting = true;
}
get contents() {
return (
<>
<OverviewStatuses/>
{ isAllowedResource("events") && <Events
compact
hideFilters
className="box grow"
/> }
</>
);
}
render() {
return (
<div className="WorkloadsOverview flex column gaps">
<OverviewStatuses/>
{isAllowedResource("events") && <Events compact hideFilters className="box grow"/>}
{this.contents}
</div>
);
}

View File

@ -19,7 +19,8 @@ import { lookupApiLink } from "../../api/kube-api";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { Badge } from "../badge";
enum columnId {
enum sortBy {
name = "name",
namespace = "namespace",
containers = "containers",
@ -76,15 +77,15 @@ export class Pods extends React.Component<Props> {
tableId = "workloads_pods"
isConfigurable
sortingCallbacks={{
[columnId.name]: (pod: Pod) => pod.getName(),
[columnId.namespace]: (pod: Pod) => pod.getNs(),
[columnId.containers]: (pod: Pod) => pod.getContainers().length,
[columnId.restarts]: (pod: Pod) => pod.getRestartsCount(),
[columnId.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind),
[columnId.qos]: (pod: Pod) => pod.getQosClass(),
[columnId.node]: (pod: Pod) => pod.getNodeName(),
[columnId.age]: (pod: Pod) => pod.metadata.creationTimestamp,
[columnId.status]: (pod: Pod) => pod.getStatusMessage(),
[sortBy.name]: (pod: Pod) => pod.getName(),
[sortBy.namespace]: (pod: Pod) => pod.getNs(),
[sortBy.containers]: (pod: Pod) => pod.getContainers().length,
[sortBy.restarts]: (pod: Pod) => pod.getRestartsCount(),
[sortBy.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind),
[sortBy.qos]: (pod: Pod) => pod.getQosClass(),
[sortBy.node]: (pod: Pod) => pod.getNodeName(),
[sortBy.age]: (pod: Pod) => pod.metadata.creationTimestamp,
[sortBy.status]: (pod: Pod) => pod.getStatusMessage(),
}}
searchFilters={[
(pod: Pod) => pod.getSearchFields(),
@ -94,16 +95,16 @@ export class Pods extends React.Component<Props> {
]}
renderHeaderTitle="Pods"
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
{ className: "warning", showWithColumn: columnId.name },
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
{ title: "Containers", className: "containers", sortBy: columnId.containers, id: columnId.containers },
{ title: "Restarts", className: "restarts", sortBy: columnId.restarts, id: columnId.restarts },
{ title: "Controlled By", className: "owners", sortBy: columnId.owners, id: columnId.owners },
{ title: "Node", className: "node", sortBy: columnId.node, id: columnId.node },
{ title: "QoS", className: "qos", sortBy: columnId.qos, id: columnId.qos },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
{ title: "Name", className: "name", sortBy: sortBy.name },
{ className: "warning", showWithColumn: "name" },
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
{ title: "Containers", className: "containers", sortBy: sortBy.containers },
{ title: "Restarts", className: "restarts", sortBy: sortBy.restarts },
{ title: "Controlled By", className: "owners", sortBy: sortBy.owners },
{ title: "Node", className: "node", sortBy: sortBy.node },
{ title: "QoS", className: "qos", sortBy: sortBy.qos },
{ title: "Age", className: "age", sortBy: sortBy.age },
{ title: "Status", className: "status", sortBy: sortBy.status },
]}
renderTableContents={(pod: Pod) => [
<Badge flat key="name" label={pod.getName()} tooltip={pod.getName()} />,

View File

@ -36,14 +36,3 @@
}
}
.ItemListLayoutVisibilityMenu {
.MenuItem {
padding: 0;
}
.Checkbox {
width: 100%;
padding: var(--spacing);
cursor: pointer;
}
}

View File

@ -129,7 +129,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
if (!isClusterScoped) {
disposeOnUnmount(this, [
namespaceStore.onContextChange(() => this.loadStores())
reaction(() => namespaceStore.items.toJS(), () => this.loadStores())
]);
}
}
@ -440,9 +440,11 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
return <TableCell key={cellProps.id ?? index} {...cellProps} />;
}
})}
{isConfigurable && (
<TableCell className="menu">
{isConfigurable && this.renderColumnVisibilityMenu()}
{this.renderColumnVisibilityMenu()}
</TableCell>
)}
</TableHead>
);
}

View File

@ -34,14 +34,14 @@ export class PageFiltersStore {
namespaceStore.setContext(filteredNs);
}
}),
namespaceStore.onContextChange(namespaces => {
reaction(() => namespaceStore.contextNs.toJS(), contextNs => {
const filteredNs = this.getValues(FilterType.NAMESPACE);
const isChanged = namespaces.length !== filteredNs.length;
const isChanged = contextNs.length !== filteredNs.length;
if (isChanged) {
this.filters.replace([
...this.filters.filter(({ type }) => type !== FilterType.NAMESPACE),
...namespaces.map(ns => ({ type: FilterType.NAMESPACE, value: ns })),
...contextNs.map(ns => ({ type: FilterType.NAMESPACE, value: ns })),
]);
}
}, {

View File

@ -0,0 +1,4 @@
.MenuCheckbox {
width: 100%;
height: 100%;
}

View File

@ -74,7 +74,7 @@ export class TableCell extends React.Component<TableCellProps> {
const content = displayBooleans(displayBoolean, title || children);
return (
<div {...cellProps} className={classNames} onClick={this.onClick}>
<div {...cellProps} id={className} className={classNames} onClick={this.onClick}>
{this.renderCheckbox()}
{_nowrap ? <div className="content">{content}</div> : content}
{this.renderSortIcon()}

View File

@ -9,7 +9,7 @@ export interface ItemObject {
@autobind()
export abstract class ItemStore<T extends ItemObject = ItemObject> {
abstract loadAll(...args: any[]): Promise<void>;
abstract loadAll(): Promise<void>;
protected defaultSorting = (item: T) => item.getName();
@ -40,7 +40,8 @@ export abstract class ItemStore<T extends ItemObject = ItemObject> {
if (item) {
return item;
} else {
}
else {
const items = this.sortItems([...this.items, newItem]);
this.items.replace(items);
@ -82,7 +83,8 @@ export abstract class ItemStore<T extends ItemObject = ItemObject> {
const index = this.items.findIndex(item => item === existingItem);
this.items.splice(index, 1, item);
} else {
}
else {
let items = [...this.items, item];
if (sortItems) items = this.sortItems(items);
@ -128,7 +130,8 @@ export abstract class ItemStore<T extends ItemObject = ItemObject> {
toggleSelection(item: T) {
if (this.isSelected(item)) {
this.unselect(item);
} else {
}
else {
this.select(item);
}
}
@ -139,7 +142,8 @@ export abstract class ItemStore<T extends ItemObject = ItemObject> {
if (allSelected) {
visibleItems.forEach(this.unselect);
} else {
}
else {
visibleItems.forEach(this.select);
}
}

View File

@ -1,4 +1,3 @@
import type { Cluster } from "../main/cluster";
import { action, observable, reaction } from "mobx";
import { autobind } from "./utils";
import { KubeObject } from "./api/kube-object";
@ -7,11 +6,7 @@ import { ItemStore } from "./item.store";
import { apiManager } from "./api/api-manager";
import { IKubeApiQueryParams, KubeApi } from "./api/kube-api";
import { KubeJsonApiData } from "./api/kube-json-api";
export interface KubeObjectStoreLoadingParams {
namespaces: string[];
api?: KubeApi;
}
import { getHostedCluster } from "../common/cluster-store";
@autobind()
export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemStore<T> {
@ -80,26 +75,14 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
}
}
protected async resolveCluster(): Promise<Cluster> {
const { getHostedCluster } = await import("../common/cluster-store");
return getHostedCluster();
}
protected async loadItems({ namespaces, api }: KubeObjectStoreLoadingParams): Promise<T[]> {
const cluster = await this.resolveCluster();
if (cluster.isAllowedResource(api.kind)) {
if (api.isNamespaced) {
protected async loadItems(allowedNamespaces?: string[]): Promise<T[]> {
if (!this.api.isNamespaced || !allowedNamespaces) {
return this.api.list({}, this.query);
} else {
return Promise
.all(namespaces.map(namespace => api.list({ namespace })))
.all(allowedNamespaces.map(namespace => this.api.list({ namespace })))
.then(items => items.flat());
}
return api.list({}, this.query);
}
return [];
}
protected filterItemsOnLoad(items: T[]) {
@ -107,35 +90,30 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
}
@action
async loadAll({ namespaces: contextNamespaces }: { namespaces?: string[] } = {}) {
async loadAll() {
this.isLoading = true;
let items: T[];
try {
if (!contextNamespaces) {
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
const { allowedNamespaces, accessibleNamespaces, isAdmin } = getHostedCluster();
contextNamespaces = namespaceStore.getContextNamespaces();
if (isAdmin && accessibleNamespaces.length == 0) {
items = await this.loadItems();
} else {
items = await this.loadItems(allowedNamespaces);
}
let items = await this.loadItems({ namespaces: contextNamespaces, api: this.api });
items = this.filterItemsOnLoad(items);
items = this.sortItems(items);
this.items.replace(items);
this.isLoaded = true;
} catch (error) {
console.error("Loading store items failed", { error, store: this });
this.resetOnError(error);
} finally {
if (items) {
items = this.sortItems(items);
this.items.replace(items);
}
this.isLoading = false;
this.isLoaded = true;
}
}
protected resetOnError(error: any) {
if (error) this.reset();
}
protected async loadItem(params: { name: string; namespace?: string }): Promise<T> {
return this.api.get(params);
}
@ -220,7 +198,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
// create latest non-observable copy of items to apply updates in one action (==single render)
const items = this.items.toJS();
for (const { type, object } of this.eventsBuffer.clear()) {
for (const {type, object} of this.eventsBuffer.clear()) {
const index = items.findIndex(item => item.getId() === object.metadata?.uid);
const item = items[index];
const api = apiManager.getApiByKind(object.kind, object.apiVersion);