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

refactoring: get rid of config.store.ts (renderer), moved allowNamespaces/allowedResources to cluster.ts apis

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-23 19:33:10 +03:00
parent 407c4fb4d0
commit 3aef3700de
19 changed files with 158 additions and 281 deletions

View File

@ -1,7 +1,7 @@
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { FeatureStatusMap } from "./feature"
import type { WorkspaceId } from "../common/workspace-store";
import { action, computed, observable, reaction, toJS, when } from "mobx";
import { action, computed, observable, reaction, toJS } from "mobx";
import { apiKubePrefix } from "../common/vars";
import { broadcastIpc } from "../common/ipc";
import { ContextHandler } from "./context-handler"
@ -20,17 +20,19 @@ export enum ClusterStatus {
}
export interface ClusterState extends ClusterModel {
initialized?: boolean;
initialized: boolean;
apiUrl: string;
online?: boolean;
accessible?: boolean;
failureReason?: string;
nodes?: number;
eventCount?: number;
version?: string;
distribution?: string;
isAdmin?: boolean;
features?: FeatureStatusMap;
online: boolean;
accessible: boolean;
failureReason: string;
nodes: number;
eventCount: number;
version: string;
distribution: string;
isAdmin: boolean;
allowedNamespaces: string[]
allowedResources: string[]
features: FeatureStatusMap;
}
export class Cluster implements ClusterModel {
@ -58,6 +60,8 @@ export class Cluster implements ClusterModel {
@observable eventCount = 0;
@observable preferences: ClusterPreferences = {};
@observable features: FeatureStatusMap = {};
@observable allowedNamespaces: string[] = [];
@observable allowedResources: string[] = [];
constructor(model: ClusterModel) {
this.updateModel(model);
@ -120,7 +124,6 @@ export class Cluster implements ClusterModel {
}
async activate() {
await when(() => this.initialized);
if (this.disconnected) await this.reconnect();
await this.refresh();
return this.pushState();
@ -150,14 +153,28 @@ export class Cluster implements ClusterModel {
this.online = connectionStatus > ClusterStatus.Offline;
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
if (this.accessible) {
this.distribution = this.detectKubernetesDistribution(this.version)
this.features = await getFeatures(this)
this.isAdmin = await this.isClusterAdmin()
this.nodes = await this.getNodeCount()
this.kubeCtl = new Kubectl(this.version)
this.kubeCtl.ensureKubectl()
this.distribution = this.detectKubernetesDistribution(this.version)
const [features, isAdmin, nodesCount] = await Promise.all([
getFeatures(this),
this.isClusterAdmin(),
this.getNodeCount(),
this.kubeCtl.ensureKubectl()
]);
this.features = features;
this.isAdmin = isAdmin;
this.nodes = nodesCount;
}
await this.refreshEvents();
await Promise.all([
this.refreshEvents(),
this.refreshAllowedResources(),
]);
}
@action
async refreshAllowedResources() {
this.allowedNamespaces = await this.getAllowedNamespaces();
this.allowedResources = await this.getAllowedResources();
}
@action
@ -342,6 +359,8 @@ export class Cluster implements ClusterModel {
isAdmin: this.isAdmin,
features: this.features,
eventCount: this.eventCount,
allowedNamespaces: this.allowedNamespaces,
allowedResources: this.allowedResources,
};
return toJS(state, {
recurseEverything: true
@ -368,4 +387,67 @@ export class Cluster implements ClusterModel {
online: this.online,
}
}
protected async getAllowedNamespaces() {
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api)
try {
const namespaceList = await api.listNamespace()
const nsAccessStatuses = await Promise.all(
namespaceList.body.items.map(ns => this.canI({
namespace: ns.metadata.name,
resource: "pods",
verb: "list",
}))
)
return namespaceList.body.items
.filter((ns, i) => nsAccessStatuses[i])
.map(ns => ns.metadata.name)
} catch (error) {
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName)
if (ctx.namespace) return [ctx.namespace]
return []
}
}
protected async getAllowedResources() {
// TODO: auto-populate all resources dynamically
const apiResources = [
{ 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: "namespaces" },
{ resource: "networkpolicies", group: "networking.k8s.io" },
{ resource: "nodes" },
{ resource: "persistentvolumes" },
{ resource: "pods" },
{ resource: "podsecuritypolicies" },
{ resource: "resourcequotas" },
{ resource: "secrets" },
{ resource: "services" },
{ resource: "statefulsets", group: "apps" },
{ resource: "storageclasses", group: "storage.k8s.io" },
]
try {
const resourceAccessStatuses = await Promise.all(
apiResources.map(apiResource => this.canI({
resource: apiResource.resource,
group: apiResource.group,
verb: "list",
namespace: this.allowedNamespaces[0]
}))
)
return apiResources
.filter((resource, i) => resourceAccessStatuses[i])
.map(apiResource => apiResource.resource)
} catch (error) {
return []
}
}
}

View File

@ -5,7 +5,7 @@ import path from "path"
import { readFile } from "fs-extra"
import { Cluster } from "./cluster"
import { apiPrefix, appName, outDir } from "../common/vars";
import { configRoute, helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
export interface RouterRequestOpts {
req: http.IncomingMessage;
@ -112,7 +112,6 @@ export class Router {
this.handleStaticFile(params.path, response);
});
this.router.add({ method: "get", path: `${apiPrefix}/config` }, configRoute.routeConfig.bind(configRoute))
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute))
// Watch API

View File

@ -1,105 +0,0 @@
import { app } from "electron"
import { CoreV1Api } from "@kubernetes/client-node"
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { Cluster } from "../cluster"
export interface IConfigRoutePayload {
kubeVersion?: string;
clusterName?: string;
lensVersion?: string;
username?: string;
token?: string;
allowedNamespaces?: string[];
allowedResources?: string[];
isClusterAdmin?: boolean;
chartsEnabled: boolean;
kubectlAccess?: boolean; // User accessed via kubectl-lens plugin
}
// TODO: auto-populate all resources dynamically
const apiResources = [
{ 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: "namespaces" },
{ resource: "networkpolicies", group: "networking.k8s.io" },
{ resource: "nodes" },
{ resource: "persistentvolumes" },
{ resource: "pods" },
{ resource: "podsecuritypolicies" },
{ resource: "resourcequotas" },
{ resource: "secrets" },
{ resource: "services" },
{ resource: "statefulsets", group: "apps" },
{ resource: "storageclasses", group: "storage.k8s.io" },
]
async function getAllowedNamespaces(cluster: Cluster) {
const api = cluster.getProxyKubeconfig().makeApiClient(CoreV1Api)
try {
const namespaceList = await api.listNamespace()
const nsAccessStatuses = await Promise.all(
namespaceList.body.items.map(ns => cluster.canI({
namespace: ns.metadata.name,
resource: "pods",
verb: "list",
}))
)
return namespaceList.body.items
.filter((ns, i) => nsAccessStatuses[i])
.map(ns => ns.metadata.name)
} catch (error) {
const ctx = cluster.getProxyKubeconfig().getContextObject(cluster.contextName)
if (ctx.namespace) {
return [ctx.namespace]
} else {
return []
}
}
}
async function getAllowedResources(cluster: Cluster, namespaces: string[]) {
try {
const resourceAccessStatuses = await Promise.all(
apiResources.map(apiResource => cluster.canI({
resource: apiResource.resource,
group: apiResource.group,
verb: "list",
namespace: namespaces[0]
}))
)
return apiResources
.filter((resource, i) => resourceAccessStatuses[i]).map(apiResource => apiResource.resource)
} catch (error) {
return []
}
}
class ConfigRoute extends LensApi {
public async routeConfig(request: LensApiRequest) {
const { params, response, cluster } = request
const namespaces = await getAllowedNamespaces(cluster)
const data: IConfigRoutePayload = {
clusterName: cluster.contextName,
lensVersion: app.getVersion(),
kubeVersion: cluster.version,
chartsEnabled: true,
isClusterAdmin: cluster.isAdmin,
allowedResources: await getAllowedResources(cluster, namespaces),
allowedNamespaces: namespaces
};
this.respondJson(response, data)
}
}
export const configRoute = new ConfigRoute()

View File

@ -1,4 +1,3 @@
export * from "./config-route"
export * from "./kubeconfig-route"
export * from "./metrics-route"
export * from "./port-forward-route"

View File

@ -6,9 +6,9 @@ import { autobind, EventEmitter } from "../utils";
import { KubeJsonApiData } from "./kube-json-api";
import type { KubeObjectStore } from "../kube-object.store";
import { KubeApi } from "./kube-api";
import { configStore } from "../config.store";
import { apiManager } from "./api-manager";
import { apiPrefix, isDevelopment } from "../../common/vars";
import { clusterStore } from "../../common/cluster-store";
export interface IKubeWatchEvent<T = any> {
type: "ADDED" | "MODIFIED" | "DELETED";
@ -61,10 +61,10 @@ export class KubeWatchApi {
}
protected getQuery(): Partial<IKubeWatchRouteQuery> {
const { isClusterAdmin, allowedNamespaces } = configStore;
const { isAdmin, allowedNamespaces } = clusterStore.activeCluster;
return {
api: this.activeApis.map(api => {
if (isClusterAdmin) return api.getWatchUrl();
if (isAdmin) return api.getWatchUrl();
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace))
}).flat()
}

View File

@ -1,10 +1,12 @@
import { configStore } from "../config.store";
import { clusterStore } from "../../common/cluster-store";
// todo: refactor / move to cluster-store.ts?
export function isAllowedResource(resources: string | string[]) {
if (!Array.isArray(resources)) {
resources = [resources];
}
const { allowedResources } = configStore;
const allowedResources = clusterStore.activeCluster?.allowedResources || [];
for (const resource of resources) {
if (!allowedResources.includes(resource)) {
return false;

View File

@ -1,7 +1,6 @@
import { stringify } from "querystring";
import { autobind, base64, EventEmitter, interval } from "../utils";
import { autobind, base64, EventEmitter } from "../utils";
import { WebSocketApi } from "./websocket-api";
import { configStore } from "../config.store";
import isEqual from "lodash/isEqual"
import { isDevelopment } from "../../common/vars";
@ -25,21 +24,19 @@ enum TerminalColor {
NO_COLOR = "\u001b[0m",
}
export interface ITerminalApiOptions {
export type TerminalApiQuery = Record<string, string> & {
id: string;
node?: string;
colorTheme?: "light" | "dark";
type?: string | "node";
}
export class TerminalApi extends WebSocketApi {
protected size: { Width: number; Height: number };
protected currentToken: string;
protected tokenInterval = interval(60, this.sendNewToken); // refresh every minute
public onReady = new EventEmitter<[]>();
public isReady = false;
constructor(protected options: ITerminalApiOptions) {
constructor(protected options: TerminalApiQuery) {
super({
logging: isDevelopment,
flushOnOpen: false,
@ -47,50 +44,33 @@ export class TerminalApi extends WebSocketApi {
});
}
async getUrl(token: string) {
const { hostname, protocol } = location;
async getUrl() {
let { port } = location;
const { hostname, protocol } = location;
const { id, node } = this.options;
const wss = `ws${protocol === "https:" ? "s" : ""}://`;
const queryParams = { token, id };
const query: TerminalApiQuery = { id };
if (port) {
port = `:${port}`
}
if (node) {
Object.assign(queryParams, {
node: node,
type: "node"
});
query.node = node;
query.type = "node";
}
return `${wss}${hostname}${port}/api?${stringify(queryParams)}`;
return `${wss}${hostname}${port}/api?${stringify(query)}`;
}
async connect() {
const token = await configStore.getToken();
const apiUrl = await this.getUrl(token);
const { colorTheme } = this.options;
this.emitStatus("Connecting ...", {
color: colorTheme == "light" ? TerminalColor.GRAY : TerminalColor.LIGHT_GRAY
});
const apiUrl = await this.getUrl();
this.emitStatus("Connecting ...");
this.onData.addListener(this._onReady, { prepend: true });
this.currentToken = token;
this.tokenInterval.start();
return super.connect(apiUrl);
}
@autobind()
async sendNewToken() {
const token = await configStore.getToken();
if (!this.isReady || token == this.currentToken) return;
this.sendCommand(token, TerminalChannels.TOKEN);
this.currentToken = token;
}
destroy() {
if (!this.socket) return;
const exitCode = String.fromCharCode(4); // ctrl+d
this.sendCommand(exitCode);
this.tokenInterval.stop();
setTimeout(() => super.destroy(), 2000);
}

View File

@ -1,11 +1,11 @@
import isEqual from "lodash/isEqual";
import { action, observable, when, IReactionDisposer, reaction } from "mobx";
import { action, IReactionDisposer, observable, reaction, when } from "mobx";
import { autobind } from "../../utils";
import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api";
import { ItemStore } from "../../item.store";
import { configStore } from "../../config.store";
import { secretsStore } from "../+config-secrets/secrets.store";
import { Secret } from "../../api/endpoints";
import { secretsStore } from "../+config-secrets/secrets.store";
import { clusterStore } from "../../../common/cluster-store";
@autobind()
export class ReleaseStore extends ItemStore<HelmRelease> {
@ -58,8 +58,8 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
this.isLoading = true;
let items;
try {
const { isClusterAdmin, allowedNamespaces } = configStore;
items = await this.loadItems(!isClusterAdmin ? allowedNamespaces : null);
const { isAdmin, allowedNamespaces } = clusterStore.activeCluster;
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
} finally {
if (items) {
items = this.sortItems(items);
@ -73,8 +73,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
async loadItems(namespaces?: string[]) {
if (!namespaces) {
return helmReleasesApi.list();
}
else {
} else {
return Promise
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
.then(items => items.flat());

View File

@ -1,7 +1,7 @@
import "./cluster.scss"
import React from "react";
import { computed, reaction, when } from "mobx";
import { computed, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { MainLayout } from "../layout/main-layout";
import { ClusterIssues } from "./cluster-issues";
@ -13,22 +13,23 @@ import { nodesStore } from "../+nodes/nodes.store";
import { podsStore } from "../+workloads-pods/pods.store";
import { clusterStore } from "./cluster.store";
import { eventStore } from "../+events/event.store";
import { configStore } from "../../config.store";
import { isAllowedResource } from "../../api/rbac";
@observer
export class Cluster extends React.Component {
private dependentStores = [nodesStore, podsStore];
private watchers = [
interval(60, () => clusterStore.getMetrics()),
interval(20, () => eventStore.loadAll())
];
private dependentStores = [nodesStore, podsStore];
@computed get isLoaded() {
return nodesStore.isLoaded && podsStore.isLoaded
}
// todo: refactor
async componentDidMount() {
await when(() => configStore.isLoaded);
const { dependentStores } = this;
if (!isAllowedResource("nodes")) {
dependentStores.splice(dependentStores.indexOf(nodesStore), 1)
@ -50,13 +51,6 @@ export class Cluster extends React.Component {
])
}
@computed get isLoaded() {
return (
nodesStore.isLoaded &&
podsStore.isLoaded
)
}
render() {
const { isLoaded } = this;
return (

View File

@ -82,10 +82,6 @@ export class Preferences extends React.Component {
}
}
onThemeChange = ({ value }: SelectOption<string>) => {
userStore.preferences.colorTheme = value;
}
onRepoSelect = async ({ value: repo }: SelectOption<HelmRepo>) => {
const isAdded = this.helmAddedRepos.has(repo.name);
if (isAdded) {

View File

@ -9,9 +9,9 @@ import { MainLayout, TabRoute } from "../layout/main-layout";
import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
import { configStore } from "../../config.store";
import { namespaceStore } from "../+namespaces/namespace.store";
import { storageURL } from "./storage.route";
import { isAllowedResource } from "../../api/rbac";
interface Props extends RouteComponentProps<{}> {
}
@ -20,7 +20,6 @@ interface Props extends RouteComponentProps<{}> {
export class Storage extends React.Component<Props> {
static get tabRoutes() {
const tabRoutes: TabRoute[] = [];
const { allowedResources } = configStore;
const query = namespaceStore.getContextParams()
tabRoutes.push({
@ -30,7 +29,7 @@ export class Storage extends React.Component<Props> {
path: volumeClaimsRoute.path,
})
if (allowedResources.includes('persistentvolumes')) {
if (isAllowedResource('persistentvolumes')) {
tabRoutes.push({
title: <Trans>Persistent Volumes</Trans>,
component: PersistentVolumes,
@ -39,7 +38,7 @@ export class Storage extends React.Component<Props> {
});
}
if (allowedResources.includes('storageclasses')) {
if (isAllowedResource('storageclasses')) {
tabRoutes.push({
title: <Trans>Storage Classes</Trans>,
component: StorageClasses,

View File

@ -1,5 +1,4 @@
import "./user-management.scss"
import React from "react";
import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
@ -11,8 +10,8 @@ import { RoleBindings } from "../+user-management-roles-bindings";
import { ServiceAccounts } from "../+user-management-service-accounts";
import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL, usersManagementURL } from "./user-management.routes";
import { namespaceStore } from "../+namespaces/namespace.store";
import { configStore } from "../../config.store";
import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies";
import { isAllowedResource } from "../../api/rbac";
interface Props extends RouteComponentProps<{}> {
}
@ -21,7 +20,6 @@ interface Props extends RouteComponentProps<{}> {
export class UserManagement extends React.Component<Props> {
static get tabRoutes() {
const tabRoutes: TabRoute[] = [];
const { allowedResources } = configStore;
const query = namespaceStore.getContextParams()
tabRoutes.push(
{
@ -43,7 +41,7 @@ export class UserManagement extends React.Component<Props> {
path: rolesRoute.path,
},
)
if (allowedResources.includes("podsecuritypolicies")) {
if (isAllowedResource("podsecuritypolicies")) {
tabRoutes.push({
title: <Trans>Pod Security Policies</Trans>,
component: PodSecurityPolicies,

View File

@ -15,13 +15,11 @@ import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
import { namespaceStore } from "../+namespaces/namespace.store";
import { PageFiltersList } from "../item-object-list/page-filters-list";
import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
import { configStore } from "../../config.store";
import { isAllowedResource } from "../../api/rbac";
@observer
export class OverviewStatuses extends React.Component {
render() {
const { allowedResources } = configStore;
const { contextNs } = namespaceStore;
const pods = isAllowedResource("pods") ? podsStore.getAllByNs(contextNs) : [];
const deployments = isAllowedResource("deployments") ? deploymentStore.getAllByNs(contextNs) : [];
@ -37,37 +35,37 @@ export class OverviewStatuses extends React.Component {
</div>
<PageFiltersList/>
<div className="workloads">
{ isAllowedResource("pods") &&
{isAllowedResource("pods") &&
<div className="workload">
<div className="title"><Link to={podsURL()}><Trans>Pods</Trans> ({pods.length})</Link></div>
<OverviewWorkloadStatus status={podsStore.getStatuses(pods)}/>
</div>
}
{ isAllowedResource("deployments") &&
{isAllowedResource("deployments") &&
<div className="workload">
<div className="title"><Link to={deploymentsURL()}><Trans>Deployments</Trans> ({deployments.length})</Link></div>
<OverviewWorkloadStatus status={deploymentStore.getStatuses(deployments)}/>
</div>
}
{ isAllowedResource("statefulsets") &&
{isAllowedResource("statefulsets") &&
<div className="workload">
<div className="title"><Link to={statefulSetsURL()}><Trans>StatefulSets</Trans> ({statefulSets.length})</Link></div>
<OverviewWorkloadStatus status={statefulSetStore.getStatuses(statefulSets)}/>
</div>
}
{ isAllowedResource("daemonsets") &&
{isAllowedResource("daemonsets") &&
<div className="workload">
<div className="title"><Link to={daemonSetsURL()}><Trans>DaemonSets</Trans> ({daemonSets.length})</Link></div>
<OverviewWorkloadStatus status={daemonSetStore.getStatuses(daemonSets)}/>
</div>
}
{ isAllowedResource("jobs") &&
{isAllowedResource("jobs") &&
<div className="workload">
<div className="title"><Link to={jobsURL()}><Trans>Jobs</Trans> ({jobs.length})</Link></div>
<OverviewWorkloadStatus status={jobStore.getStatuses(jobs)}/>
</div>
}
{ isAllowedResource("cronjobs") &&
{isAllowedResource("cronjobs") &&
<div className="workload">
<div className="title"><Link to={cronJobsURL()}><Trans>CronJobs</Trans> ({cronJobs.length})</Link></div>
<OverviewWorkloadStatus status={cronJobStore.getStatuses(cronJobs)}/>

View File

@ -1,6 +1,7 @@
import "./app.scss";
import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
import { computed, observable, reaction } from "mobx";
import { Redirect, Route, Switch } from "react-router";
import { Notifications } from "./notifications";
import { NotFound } from "./+404";
@ -31,14 +32,12 @@ import { LandingPage, landingRoute, landingURL } from "./+landing-page";
import { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings";
import { Workspaces, workspacesRoute } from "./+workspaces";
import { ErrorBoundary } from "./error-boundary";
import { computed, observable, reaction } from "mobx";
import { configStore } from "../config.store";
import { navigation } from "../navigation";
import { clusterIpc } from "../../common/cluster-ipc";
import { clusterStore } from "../../common/cluster-store";
import { ClusterStatus } from "./cluster-manager/cluster-status";
import { clusterStatusRoute, clusterStatusURL } from "./cluster-manager/cluster-status.route";
import { Preferences, preferencesRoute } from "./+preferences";
import { navigation } from "../navigation";
import { ClusterStatus } from "./cluster-manager/cluster-status";
import { CubeSpinner } from "./spinner";
@observer
@ -52,7 +51,6 @@ export class App extends React.Component {
async componentDidMount() {
await clusterIpc.activate.invokeFromRenderer();
await configStore.init();
this.appReady = true;
disposeOnUnmount(this, [

View File

@ -6,7 +6,6 @@ import { TerminalApi } from "../../api/terminal-api";
import { dockStore, IDockTab, TabId, TabKind } from "./dock.store";
import { WebSocketApiState } from "../../api/websocket-api";
import { _i18n } from "../../i18n";
import { themeStore } from "../../theme.store";
export interface ITerminalTab extends IDockTab {
node?: string; // activate node shell mode
@ -16,7 +15,6 @@ export function isTerminalTab(tab: IDockTab) {
return tab && tab.kind === TabKind.TERMINAL;
}
export function createTerminalTab(tabParams: Partial<ITerminalTab> = {}) {
return dockStore.createTab({
kind: TabKind.TERMINAL,
@ -56,7 +54,6 @@ export class TerminalStore {
const api = new TerminalApi({
id: tabId,
node: tab.node,
colorTheme: themeStore.activeTheme.type
});
const terminal = new Terminal(tabId, api);
this.connections.set(tabId, api);

View File

@ -19,7 +19,6 @@ import { PageFiltersList } from "./page-filters-list";
import { PageFiltersSelect } from "./page-filters-select";
import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
import { themeStore } from "../../theme.store";
import { configStore } from "../../config.store";
// todo: refactor, split to small re-usable components
@ -116,7 +115,6 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
const stores = [store, ...dependentStores];
if (!isClusterScoped) stores.push(namespaceStore);
try {
await when(() => configStore.isLoaded); // todo: remove
await Promise.all(stores.map(store => store.loadAll()));
const subscriptions = stores.map(store => store.subscribe());
await when(() => this.isUnmounting);

View File

@ -7,11 +7,11 @@ import { matchPath, RouteProps } from "react-router-dom";
import { createStorage, cssNames } from "../../utils";
import { Tab, Tabs } from "../tabs";
import { Sidebar } from "./sidebar";
import { configStore } from "../../config.store";
import { ErrorBoundary } from "../error-boundary";
import { Dock } from "../dock";
import { navigate, navigation } from "../../navigation";
import { themeStore } from "../../theme.store";
import { clusterStore } from "../../../common/cluster-store";
export interface TabRoute extends RouteProps {
title: React.ReactNode;
@ -47,14 +47,12 @@ export class MainLayout extends React.Component<Props> {
render() {
const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props;
const { clusterName } = configStore.config;
const { pathname } = navigation.location;
const clusterName = clusterStore.activeCluster?.contextName;
const routePath = navigation.location.pathname;
return (
<div className={cssNames("MainLayout", className, themeStore.activeTheme.type)}>
<header className={cssNames("flex gaps align-center", headerClass)}>
<div className="box grow flex align-center">
{clusterName && <span>{clusterName}</span>}
</div>
<span className="cluster">{clusterName}</span>
</header>
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>
@ -68,7 +66,7 @@ export class MainLayout extends React.Component<Props> {
{tabs && (
<Tabs center onChange={url => navigate(url)}>
{tabs.map(({ title, path, url, ...routeProps }) => {
const isActive = !!matchPath(pathname, { path, ...routeProps });
const isActive = !!matchPath(routePath, { path, ...routeProps });
return <Tab key={url} label={title} value={url} active={isActive}/>
})}
</Tabs>

View File

@ -1,51 +0,0 @@
import type { IConfigRoutePayload } from "../main/routes/config-route";
import { observable, when } from "mobx";
import { autobind, interval } from "./utils";
import { apiBase } from "./api";
@autobind()
export class ConfigStore {
protected updater = interval(60, this.load);
@observable config: Partial<IConfigRoutePayload> = {};
@observable isLoaded = false;
async init() {
await this.load();
this.updater.start();
}
load() {
if (location.hostname === "no-clusters.localhost") {
return;
}
return apiBase.get("/config").then((config: IConfigRoutePayload) => {
this.config = config;
this.isLoaded = true;
});
}
async getToken() {
await when(() => this.isLoaded);
return this.config.token;
}
get allowedNamespaces() {
return this.config.allowedNamespaces || [];
}
get allowedResources() {
return this.config.allowedResources || [];
}
get isClusterAdmin() {
return this.config.isClusterAdmin;
}
reset() {
this.isLoaded = false;
this.config = {};
}
}
export const configStore = new ConfigStore();

View File

@ -3,10 +3,10 @@ import { autobind } from "./utils";
import { KubeObject } from "./api/kube-object";
import { IKubeWatchEvent, kubeWatchApi } from "./api/kube-watch-api";
import { ItemStore } from "./item.store";
import { configStore } from "./config.store";
import { apiManager } from "./api/api-manager";
import { IKubeApiQueryParams, KubeApi } from "./api/kube-api";
import { KubeJsonApiData } from "./api/kube-json-api";
import { clusterStore } from "../common/cluster-store";
@autobind()
export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemStore<T> {
@ -23,8 +23,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
const namespaces: string[] = [].concat(namespace);
if (namespaces.length) {
return this.items.filter(item => namespaces.includes(item.getNs()));
}
else if (!strict) {
} else if (!strict) {
return this.items;
}
}
@ -47,8 +46,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
const itemLabels = item.getLabels();
return labels.every(label => itemLabels.includes(label));
})
}
else {
} else {
return this.items.filter((item: T) => {
const itemLabels = item.metadata.labels || {};
return Object.entries(labels)
@ -62,8 +60,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
const { limit } = this;
const query: IKubeApiQueryParams = limit ? { limit } : {};
return this.api.list({}, query);
}
else {
} else {
return Promise
.all(allowedNamespaces.map(namespace => this.api.list({ namespace })))
.then(items => items.flat())
@ -79,8 +76,8 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
this.isLoading = true;
let items: T[];
try {
const { isClusterAdmin, allowedNamespaces } = configStore;
items = await this.loadItems(!isClusterAdmin ? allowedNamespaces : null);
const { isAdmin, allowedNamespaces } = clusterStore.activeCluster;
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
items = this.filterItemsOnLoad(items);
} finally {
if (items) {
@ -180,8 +177,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
const newItem = new api.objectConstructor(object);
if (!item) {
items.push(newItem);
}
else {
} else {
items.splice(index, 1, newItem);
}
break;