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 committed 2020-07-23 19:33:10 +03:00
1 parent 407c4fb4d0
commit 3aef3700de
19 files changed
+158 -281

No files matched your search

+3 -3
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()
}
+4 -2
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;
+12 -32
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);
}
@@ -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());
+6 -12
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 (
@@ -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) {
+3 -4
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,
@@ -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,
@@ -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)}/>
+3 -5
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, [
@@ -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);
@@ -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);
@@ -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>
-51
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();
+7 -11
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;