mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
refactoring, detaching NamespaceStore from KubeObjectStore
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
5b86ae2f5d
commit
77e3a7fb83
@ -1,14 +1,14 @@
|
|||||||
// Kubernetes watch-api client
|
// Kubernetes watch-api client
|
||||||
// API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
|
// API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
|
||||||
|
|
||||||
import type { Cluster } from "../../main/cluster";
|
|
||||||
import type { IKubeWatchEvent, IKubeWatchEventStreamEnd, IWatchRoutePayload } from "../../main/routes/watch-route";
|
import type { IKubeWatchEvent, IKubeWatchEventStreamEnd, IWatchRoutePayload } from "../../main/routes/watch-route";
|
||||||
import type { KubeObject } from "./kube-object";
|
import type { KubeObject } from "./kube-object";
|
||||||
import type { KubeObjectStore } from "../kube-object.store";
|
import type { KubeObjectStore } from "../kube-object.store";
|
||||||
|
import type { ClusterContext } from "../components/context";
|
||||||
|
|
||||||
import plimit from "p-limit";
|
import plimit from "p-limit";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
import { autorun, comparer, computed, IReactionDisposer, observable, reaction } from "mobx";
|
import { comparer, computed, IReactionDisposer, observable, reaction, when } from "mobx";
|
||||||
import { autobind, EventEmitter, noop } from "../utils";
|
import { autobind, EventEmitter, noop } from "../utils";
|
||||||
import { ensureObjectSelfLink, KubeApi, parseKubeApi } from "./kube-api";
|
import { ensureObjectSelfLink, KubeApi, parseKubeApi } from "./kube-api";
|
||||||
import { KubeJsonApiData, KubeJsonApiError } from "./kube-json-api";
|
import { KubeJsonApiData, KubeJsonApiError } from "./kube-json-api";
|
||||||
@ -48,21 +48,18 @@ export class KubeWatchApi {
|
|||||||
private reader: ReadableStreamReader<string>;
|
private reader: ReadableStreamReader<string>;
|
||||||
public onMessage = new EventEmitter<[IKubeWatchMessage]>();
|
public onMessage = new EventEmitter<[IKubeWatchMessage]>();
|
||||||
|
|
||||||
@observable.ref private cluster: Cluster;
|
@observable context: ClusterContext = null;
|
||||||
@observable.ref private namespaces: string[] = [];
|
|
||||||
@observable subscribers = observable.map<KubeApi, number>();
|
@observable subscribers = observable.map<KubeApi, number>();
|
||||||
@observable isConnected = false;
|
@observable isConnected = false;
|
||||||
|
|
||||||
@computed get isReady(): boolean {
|
contextReady = when(() => Boolean(this.context));
|
||||||
return Boolean(this.cluster && this.namespaces);
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get isActive(): boolean {
|
@computed get isActive(): boolean {
|
||||||
return this.apis.length > 0;
|
return this.apis.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get apis(): string[] {
|
@computed get apis(): string[] {
|
||||||
if (!this.isReady) {
|
if (!this.context) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,22 +69,20 @@ export class KubeWatchApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: optimize - check when all namespaces are selected and then request all in one
|
// TODO: optimize - check when all namespaces are selected and then request all in one
|
||||||
if (api.isNamespaced && !this.cluster.isGlobalWatchEnabled) {
|
if (api.isNamespaced && !this.context.cluster.isGlobalWatchEnabled) {
|
||||||
return this.namespaces.map(namespace => api.getWatchUrl(namespace));
|
return this.context.contextNamespaces.map(namespace => api.getWatchUrl(namespace));
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.getWatchUrl();
|
return api.getWatchUrl();
|
||||||
}).flat();
|
}).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
async init({ getCluster, getNamespaces }: {
|
constructor() {
|
||||||
getCluster: () => Cluster,
|
this.init();
|
||||||
getNamespaces: () => string[],
|
}
|
||||||
}): Promise<void> {
|
|
||||||
autorun(() => {
|
private async init() {
|
||||||
this.cluster = getCluster();
|
await this.contextReady;
|
||||||
this.namespaces = getNamespaces();
|
|
||||||
});
|
|
||||||
this.bindAutoConnect();
|
this.bindAutoConnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +104,7 @@ export class KubeWatchApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isAllowedApi(api: KubeApi): boolean {
|
isAllowedApi(api: KubeApi): boolean {
|
||||||
return Boolean(this?.cluster.isAllowedResource(api.kind));
|
return Boolean(this.context?.cluster.isAllowedResource(api.kind));
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeApi(api: KubeApi | KubeApi[]): () => void {
|
subscribeApi(api: KubeApi | KubeApi[]): () => void {
|
||||||
@ -130,15 +125,15 @@ export class KubeWatchApi {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
preloadStores(stores: KubeObjectStore[], { loadOnce = false } = {}) {
|
preloadStores(stores: KubeObjectStore[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) {
|
||||||
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
|
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
|
||||||
const preloading: Promise<any>[] = [];
|
const preloading: Promise<any>[] = [];
|
||||||
|
|
||||||
for (const store of stores) {
|
for (const store of stores) {
|
||||||
preloading.push(limitRequests(async () => {
|
preloading.push(limitRequests(async () => {
|
||||||
if (store.isLoaded && loadOnce) return; // skip
|
if (store.isLoaded && opts.loadOnce) return; // skip
|
||||||
|
|
||||||
return store.loadAll(this.namespaces);
|
return store.loadAll({ namespaces: opts.namespaces });
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +149,7 @@ export class KubeWatchApi {
|
|||||||
const unsubscribeList: (() => void)[] = [];
|
const unsubscribeList: (() => void)[] = [];
|
||||||
let isUnsubscribed = false;
|
let isUnsubscribed = false;
|
||||||
|
|
||||||
const load = () => this.preloadStores(stores, { loadOnce });
|
const load = (namespaces?: string[]) => this.preloadStores(stores, { namespaces, loadOnce });
|
||||||
let preloading = preload && load();
|
let preloading = preload && load();
|
||||||
let cancelReloading: IReactionDisposer = noop;
|
let cancelReloading: IReactionDisposer = noop;
|
||||||
|
|
||||||
@ -175,10 +170,10 @@ export class KubeWatchApi {
|
|||||||
subscribe();
|
subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
// reload when context namespaces changes
|
// partial reload only selected namespaces
|
||||||
cancelReloading = reaction(() => this.namespaces, () => {
|
cancelReloading = reaction(() => this.context.contextNamespaces, namespaces => {
|
||||||
preloading?.cancelLoading();
|
preloading?.cancelLoading();
|
||||||
preloading = load();
|
preloading = load(namespaces);
|
||||||
}, {
|
}, {
|
||||||
equals: comparer.shallow,
|
equals: comparer.shallow,
|
||||||
});
|
});
|
||||||
@ -291,7 +286,7 @@ export class KubeWatchApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// skip updates from non-watching resources context
|
// skip updates from non-watching resources context
|
||||||
if (!namespace || this.namespaces.includes(namespace)) {
|
if (!namespace || this.context?.contextNamespaces.includes(namespace)) {
|
||||||
this.onMessage.emit(message);
|
this.onMessage.emit(message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
const { store } = this;
|
const { store } = this;
|
||||||
|
|
||||||
if (store && !store.isLoading && !store.isLoaded) {
|
if (store && !store.isLoading && !store.isLoaded) {
|
||||||
store.loadAllFromContextNamespaces();
|
store.reloadAll();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
@ -97,7 +97,7 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
...extraColumns.map((column) => {
|
...extraColumns.map((column) => {
|
||||||
let value = jsonPath.value(crdInstance, parseJsonPath(column.jsonPath.slice(1)));
|
let value = jsonPath.value(crdInstance, parseJsonPath(column.jsonPath.slice(1)));
|
||||||
|
|
||||||
if (Array.isArray(value) || typeof value === "object") {
|
if (Array.isArray(value) || typeof value === "object") {
|
||||||
value = JSON.stringify(value);
|
value = JSON.stringify(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export interface KubeEventDetailsProps {
|
|||||||
@observer
|
@observer
|
||||||
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
eventStore.loadAllFromContextNamespaces();
|
eventStore.reloadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -32,8 +32,8 @@ export class NamespaceDetails extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
resourceQuotaStore.loadAllFromContextNamespaces();
|
resourceQuotaStore.reloadAll();
|
||||||
limitRangeStore.loadAllFromContextNamespaces();
|
limitRangeStore.reloadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -41,8 +41,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async init() {
|
private async init() {
|
||||||
await this.resolveCluster();
|
if (!this.context) return; // skip for non-cluster context window
|
||||||
if (!this.cluster) return; // skip for non-cluster context window
|
|
||||||
|
|
||||||
this.setContext(this.initialNamespaces);
|
this.setContext(this.initialNamespaces);
|
||||||
this.autoLoadAllowedNamespaces();
|
this.autoLoadAllowedNamespaces();
|
||||||
@ -66,7 +65,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private autoLoadAllowedNamespaces(): IReactionDisposer {
|
private autoLoadAllowedNamespaces(): IReactionDisposer {
|
||||||
return reaction(() => this.allowedNamespaces, namespaces => this.loadAll(namespaces), {
|
return reaction(() => this.allowedNamespaces, namespaces => this.loadAll({ namespaces }), {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
equals: comparer.shallow,
|
equals: comparer.shallow,
|
||||||
});
|
});
|
||||||
@ -94,8 +93,8 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
|
|
||||||
@computed get allowedNamespaces(): string[] {
|
@computed get allowedNamespaces(): string[] {
|
||||||
return Array.from(new Set([
|
return Array.from(new Set([
|
||||||
...(this.cluster?.allowedNamespaces ?? []), // loaded names from main, updating every 30s and thus might be stale
|
...(this.context?.allNamespaces ?? []), // allowed namespaces from cluster (main), updating every 30s
|
||||||
...this.items.map(item => item.getName()), // loaded names from hosted cluster
|
...this.items.map(item => item.getName()), // loaded namespaces from k8s api
|
||||||
].flat()));
|
].flat()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +110,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
|
|
||||||
getSubscribeApis() {
|
getSubscribeApis() {
|
||||||
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
|
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
|
||||||
if (this.cluster?.accessibleNamespaces.length > 0) {
|
if (this.context?.cluster.accessibleNamespaces.length > 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export class NodeDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
podsStore.loadAllFromContextNamespaces();
|
podsStore.reloadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { Dialog, DialogProps } from "../dialog";
|
|||||||
import { Wizard, WizardStep } from "../wizard";
|
import { Wizard, WizardStep } from "../wizard";
|
||||||
import { Select, SelectOption } from "../select";
|
import { Select, SelectOption } from "../select";
|
||||||
import { SubTitle } from "../layout/sub-title";
|
import { SubTitle } from "../layout/sub-title";
|
||||||
import { IRoleBindingSubject, RoleBinding, ServiceAccount, Role } from "../../api/endpoints";
|
import { IRoleBindingSubject, Role, RoleBinding, ServiceAccount } from "../../api/endpoints";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { Input } from "../input";
|
import { Input } from "../input";
|
||||||
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
||||||
@ -19,6 +19,7 @@ import { namespaceStore } from "../+namespaces/namespace.store";
|
|||||||
import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store";
|
import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store";
|
||||||
import { roleBindingsStore } from "./role-bindings.store";
|
import { roleBindingsStore } from "./role-bindings.store";
|
||||||
import { showDetails } from "../kube-object";
|
import { showDetails } from "../kube-object";
|
||||||
|
import { KubeObjectStore } from "../../kube-object.store";
|
||||||
|
|
||||||
interface BindingSelectOption extends SelectOption {
|
interface BindingSelectOption extends SelectOption {
|
||||||
value: string; // binding name
|
value: string; // binding name
|
||||||
@ -73,14 +74,14 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async loadData() {
|
async loadData() {
|
||||||
const stores = [
|
const stores: KubeObjectStore[] = [
|
||||||
namespaceStore,
|
namespaceStore,
|
||||||
rolesStore,
|
rolesStore,
|
||||||
serviceAccountsStore,
|
serviceAccountsStore,
|
||||||
];
|
];
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
await Promise.all(stores.map(store => store.loadAllFromContextNamespaces()));
|
await Promise.all(stores.map(store => store.reloadAll()));
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,8 +137,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
|||||||
roleBinding: this.roleBinding,
|
roleBinding: this.roleBinding,
|
||||||
addSubjects: subjects,
|
addSubjects: subjects,
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
const name = useRoleForBindingName ? selectedRole.getName() : bindingName;
|
const name = useRoleForBindingName ? selectedRole.getName() : bindingName;
|
||||||
|
|
||||||
roleBinding = await roleBindingsStore.create({ name, namespace }, {
|
roleBinding = await roleBindingsStore.create({ name, namespace }, {
|
||||||
@ -265,7 +265,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
|||||||
</h5>
|
</h5>
|
||||||
);
|
);
|
||||||
const disableNext = this.isLoading || !selectedRole || !selectedBindings.length;
|
const disableNext = this.isLoading || !selectedRole || !selectedBindings.length;
|
||||||
const nextLabel = isEditing ? "Update" : "Create";
|
const nextLabel = isEditing ? "Update" : "Create";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@ -20,7 +20,7 @@ interface Props extends KubeObjectDetailsProps<CronJob> {
|
|||||||
@observer
|
@observer
|
||||||
export class CronJobDetails extends React.Component<Props> {
|
export class CronJobDetails extends React.Component<Props> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
jobStore.loadAllFromContextNamespaces();
|
jobStore.reloadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export class DaemonSetDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
podsStore.loadAllFromContextNamespaces();
|
podsStore.reloadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export class DeploymentDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
podsStore.loadAllFromContextNamespaces();
|
podsStore.reloadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -25,7 +25,7 @@ interface Props extends KubeObjectDetailsProps<Job> {
|
|||||||
@observer
|
@observer
|
||||||
export class JobDetails extends React.Component<Props> {
|
export class JobDetails extends React.Component<Props> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
podsStore.loadAllFromContextNamespaces();
|
podsStore.reloadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export class ReplicaSetDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
podsStore.loadAllFromContextNamespaces();
|
podsStore.reloadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export class StatefulSetDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
podsStore.loadAllFromContextNamespaces();
|
podsStore.reloadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -43,12 +43,13 @@ import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../exte
|
|||||||
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
|
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
|
||||||
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
||||||
import { eventStore } from "./+events/event.store";
|
import { eventStore } from "./+events/event.store";
|
||||||
import { namespaceStore } from "./+namespaces/namespace.store";
|
|
||||||
import { nodesStore } from "./+nodes/nodes.store";
|
import { nodesStore } from "./+nodes/nodes.store";
|
||||||
import { podsStore } from "./+workloads-pods/pods.store";
|
import { podsStore } from "./+workloads-pods/pods.store";
|
||||||
import { kubeWatchApi } from "../api/kube-watch-api";
|
import { kubeWatchApi } from "../api/kube-watch-api";
|
||||||
import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale-dialog";
|
import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale-dialog";
|
||||||
import { CommandContainer } from "./command-palette/command-container";
|
import { CommandContainer } from "./command-palette/command-container";
|
||||||
|
import { KubeObjectStore } from "../kube-object.store";
|
||||||
|
import { clusterContext } from "./context";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class App extends React.Component {
|
export class App extends React.Component {
|
||||||
@ -76,10 +77,9 @@ export class App extends React.Component {
|
|||||||
});
|
});
|
||||||
whatInput.ask(); // Start to monitor user input device
|
whatInput.ask(); // Start to monitor user input device
|
||||||
|
|
||||||
await kubeWatchApi.init({
|
// Setup hosted cluster context
|
||||||
getCluster: () => getHostedCluster(),
|
KubeObjectStore.defaultContext = clusterContext;
|
||||||
getNamespaces: () => namespaceStore.contextNamespaces,
|
kubeWatchApi.context = clusterContext;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -162,9 +162,9 @@ export class App extends React.Component {
|
|||||||
const tabRoutes = this.getTabLayoutRoutes(menu);
|
const tabRoutes = this.getTabLayoutRoutes(menu);
|
||||||
|
|
||||||
if (tabRoutes.length > 0) {
|
if (tabRoutes.length > 0) {
|
||||||
const pageComponent = () => <TabLayout tabs={tabRoutes} />;
|
const pageComponent = () => <TabLayout tabs={tabRoutes}/>;
|
||||||
|
|
||||||
route = <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)} />;
|
route = <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)}/>;
|
||||||
this.extensionRoutes.set(menu, route);
|
this.extensionRoutes.set(menu, route);
|
||||||
} else {
|
} else {
|
||||||
const page = clusterPageRegistry.getByPageTarget(menu.target);
|
const page = clusterPageRegistry.getByPageTarget(menu.target);
|
||||||
@ -228,7 +228,7 @@ export class App extends React.Component {
|
|||||||
<StatefulSetScaleDialog/>
|
<StatefulSetScaleDialog/>
|
||||||
<ReplicaSetScaleDialog/>
|
<ReplicaSetScaleDialog/>
|
||||||
<CronJobTriggerDialog/>
|
<CronJobTriggerDialog/>
|
||||||
<CommandContainer cluster={cluster} />
|
<CommandContainer cluster={cluster}/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|||||||
23
src/renderer/components/context.ts
Executable file
23
src/renderer/components/context.ts
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
import type { Cluster } from "../../main/cluster";
|
||||||
|
import { getHostedCluster } from "../../common/cluster-store";
|
||||||
|
import { namespaceStore } from "./+namespaces/namespace.store";
|
||||||
|
|
||||||
|
export interface ClusterContext {
|
||||||
|
cluster?: Cluster;
|
||||||
|
allNamespaces?: string[]; // available / allowed namespaces from cluster.ts
|
||||||
|
contextNamespaces?: string[]; // selected by user (see: namespace-select.tsx)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clusterContext: ClusterContext = {
|
||||||
|
get cluster(): Cluster | null {
|
||||||
|
return getHostedCluster();
|
||||||
|
},
|
||||||
|
|
||||||
|
get allNamespaces(): string[] {
|
||||||
|
return this.cluster?.allowedNamespaces ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
get contextNamespaces(): string[] {
|
||||||
|
return namespaceStore.contextNamespaces ?? [];
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -40,7 +40,7 @@ interface Props {
|
|||||||
@observer
|
@observer
|
||||||
export class Sidebar extends React.Component<Props> {
|
export class Sidebar extends React.Component<Props> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
crdStore.loadAllFromContextNamespaces();
|
crdStore.reloadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCustomResources() {
|
renderCustomResources() {
|
||||||
|
|||||||
@ -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<void>;
|
abstract loadAll(...args: any[]): Promise<void | T[]>;
|
||||||
|
|
||||||
protected defaultSorting = (item: T) => item.getName();
|
protected defaultSorting = (item: T) => item.getName();
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import type { Cluster } from "../main/cluster";
|
import type { ClusterContext } from "./components/context";
|
||||||
import type { NamespaceStore } from "./components/+namespaces/namespace.store";
|
|
||||||
|
|
||||||
import { action, computed, observable, reaction } from "mobx";
|
import { action, observable, reaction, when } from "mobx";
|
||||||
import { autobind } from "./utils";
|
import { autobind } from "./utils";
|
||||||
import { KubeObject } from "./api/kube-object";
|
import { KubeObject } from "./api/kube-object";
|
||||||
import { IKubeWatchEvent, IKubeWatchMessage, kubeWatchApi } from "./api/kube-watch-api";
|
import { IKubeWatchEvent, IKubeWatchMessage, kubeWatchApi } from "./api/kube-watch-api";
|
||||||
@ -17,44 +16,23 @@ export interface KubeObjectStoreLoadingParams {
|
|||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemStore<T> {
|
export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemStore<T> {
|
||||||
|
@observable static defaultContext: ClusterContext; // TODO: support multiple cluster contexts
|
||||||
|
|
||||||
abstract api: KubeApi<T>;
|
abstract api: KubeApi<T>;
|
||||||
public readonly limit?: number;
|
public readonly limit?: number;
|
||||||
public readonly bufferSize: number = 50000;
|
public readonly bufferSize: number = 50000;
|
||||||
@observable.ref protected cluster: Cluster;
|
|
||||||
|
contextReady = when(() => Boolean(this.context));
|
||||||
|
|
||||||
|
get context(): ClusterContext {
|
||||||
|
return KubeObjectStore.defaultContext;
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.bindWatchEventsUpdater();
|
this.bindWatchEventsUpdater();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: detach / remove circular dependency
|
|
||||||
@observable.ref private namespaceStore: NamespaceStore;
|
|
||||||
|
|
||||||
protected async resolveNamespaceStore(): Promise<NamespaceStore> {
|
|
||||||
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
|
|
||||||
|
|
||||||
this.namespaceStore = namespaceStore;
|
|
||||||
|
|
||||||
return namespaceStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async resolveCluster(): Promise<Cluster> {
|
|
||||||
const { getHostedCluster, clusterStore } = await import("../common/cluster-store");
|
|
||||||
|
|
||||||
await clusterStore.whenLoaded;
|
|
||||||
this.cluster = getHostedCluster();
|
|
||||||
await this.cluster.whenReady;
|
|
||||||
|
|
||||||
return this.cluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: figure out how to transparently replace with this.items
|
|
||||||
@computed get contextItems(): T[] {
|
|
||||||
const contextNamespaces = this.namespaceStore?.contextNamespaces ?? []; // not loaded
|
|
||||||
|
|
||||||
return this.items.filter((item: T) => !item.getNs() || contextNamespaces.includes(item.getId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
get query(): IKubeApiQueryParams {
|
get query(): IKubeApiQueryParams {
|
||||||
const { limit } = this;
|
const { limit } = this;
|
||||||
|
|
||||||
@ -111,9 +89,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async loadItems({ namespaces, api }: KubeObjectStoreLoadingParams): Promise<T[]> {
|
protected async loadItems({ namespaces, api }: KubeObjectStoreLoadingParams): Promise<T[]> {
|
||||||
const cluster = await this.resolveCluster();
|
if (this.context?.cluster.isAllowedResource(api.kind)) {
|
||||||
|
|
||||||
if (cluster.isAllowedResource(api.kind)) {
|
|
||||||
if (api.isNamespaced) {
|
if (api.isNamespaced) {
|
||||||
return Promise
|
return Promise
|
||||||
.all(namespaces.map(namespace => api.list({ namespace })))
|
.all(namespaces.map(namespace => api.list({ namespace })))
|
||||||
@ -131,21 +107,24 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll(namespaces?: string[], { replace = false /*partial update*/ } = {}): Promise<void> {
|
async loadAll({ namespaces = [], merge = true } = {}): Promise<void | T[]> {
|
||||||
|
await this.contextReady;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// load all available namespaces by default
|
if (!namespaces.length) {
|
||||||
if (!namespaces?.length) {
|
namespaces = this.context.allNamespaces; // load all available namespaces by default
|
||||||
const namespaceStore = await this.resolveNamespaceStore();
|
|
||||||
|
|
||||||
namespaces = namespaceStore.allowedNamespaces; // load all by default if list not provided
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = await this.loadItems({ namespaces, api: this.api });
|
const items = await this.loadItems({ namespaces, api: this.api });
|
||||||
|
|
||||||
this.mergeItems(items, { replace });
|
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
|
|
||||||
|
if (merge) {
|
||||||
|
this.mergeItems(items);
|
||||||
|
} else {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Loading store items failed", { error, store: this });
|
console.error("Loading store items failed", { error, store: this });
|
||||||
this.resetOnError(error);
|
this.resetOnError(error);
|
||||||
@ -155,18 +134,28 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
mergeItems(partialItems: T[], { replace = false, updateStore = true, sort = true, filter = true } = {}): T[] {
|
reloadAll(opts: { namespaces?: string[], merge?: boolean, force?: boolean } = {}) {
|
||||||
|
const { force = false, ...loadingOptions } = opts;
|
||||||
|
|
||||||
|
if (this.isLoading || (this.isLoaded && !force)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.loadAll(loadingOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
mergeItems(partialItems: T[], { replace = true, updateStore = true, sort = true, filter = true } = {}): T[] {
|
||||||
let items = partialItems;
|
let items = partialItems;
|
||||||
|
|
||||||
|
// update existing items
|
||||||
if (!replace) {
|
if (!replace) {
|
||||||
items = this.items.toJS();
|
const partialIds = partialItems.map(item => item.getId());
|
||||||
|
|
||||||
partialItems.forEach(item => {
|
items = [
|
||||||
const index = items.findIndex(i => i.getId() === item.getId());
|
...this.items.filter(existingItem => !partialIds.includes(existingItem.getId())),
|
||||||
|
...partialItems,
|
||||||
if (index < 0) items.push(item); // add
|
];
|
||||||
else items[index] = item; // update
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter) items = this.filterItemsOnLoad(items);
|
if (filter) items = this.filterItemsOnLoad(items);
|
||||||
@ -176,10 +165,6 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadAllFromContextNamespaces(): Promise<void> {
|
|
||||||
return this.loadAll(this.namespaceStore?.contextNamespaces);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected resetOnError(error: any) {
|
protected resetOnError(error: any) {
|
||||||
if (error) this.reset();
|
if (error) this.reset();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user