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

window-manager refactoring

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-09 20:15:15 +03:00
parent ce1cccc965
commit cac4896517
16 changed files with 179 additions and 238 deletions

View File

@ -1,14 +1,15 @@
import url from "url"
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { FeatureStatusMap } from "./feature"
import { observable, toJS } from "mobx";
import { computed, observable, toJS } from "mobx";
import { apiPrefix } from "../common/vars";
import { ContextHandler } from "./context-handler"
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
import { Kubectl } from "./kubectl";
import { KubeconfigManager } from "./kubeconfig-manager"
import { getNodeWarningConditions, loadConfig, podHasIssues } from "./k8s"
import { getNodeWarningConditions, podHasIssues } from "./k8s"
import { getFeatures, installFeature, uninstallFeature, upgradeFeature } from "./feature-manager";
import request from "request-promise-native"
import request, { RequestPromiseOptions } from "request-promise-native"
import logger from "./logger"
enum ClusterStatus {
@ -43,7 +44,6 @@ export class Cluster implements ClusterModel {
@observable contextName: string;
@observable url: string;
@observable port: number;
@observable apiUrl: string;
@observable online: boolean;
@observable accessible: boolean;
@observable failureReason: string;
@ -63,21 +63,28 @@ export class Cluster implements ClusterModel {
Object.assign(this, model)
}
@computed get apiUrl() {
return url.parse(`http://${this.id}.localhost:${this.port}`)
}
@computed get apiServerUrl() {
return url.parse(`http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}`)
}
async init(port: number) {
const { contextName } = this
try {
const kubeConfig = loadConfig(this.kubeConfigPath)
kubeConfig.setCurrentContext(contextName); // fixme: is it required, when if so?
this.port = port;
this.apiUrl = kubeConfig.getCurrentCluster().server
this.contextHandler = new ContextHandler(kubeConfig, this)
this.contextHandler = new ContextHandler(this);
await this.contextHandler.init() // So we get the proxy port reserved
this.kubeconfigManager = new KubeconfigManager(this)
this.url = this.contextHandler.url
this.initialized = true;
logger.debug(`[CLUSTER]: init done for "${this.id}", context ${contextName}`);
logger.debug(`[CLUSTER]: init done (id="${this.id}", context="${this.contextName}")`);
} catch (err) {
logger.error(`[CLUSTER]: init "${this.id}" has failed`, { err, contextName });
logger.error(`[CLUSTER]: init failed (id="${this.id}")`, {
contextName: this.contextName,
error: err
});
}
}
@ -132,16 +139,15 @@ export class Cluster implements ClusterModel {
return this.preferences.prometheus?.prefix || ""
}
protected async k8sRequest(path: string, opts?: request.RequestPromiseOptions) {
const options = Object.assign({
k8sRequest(path: string, options: RequestPromiseOptions = {}) {
return request(this.apiServerUrl + path, {
json: true,
timeout: 10000
}, (opts || {}))
if (!options.headers) {
options.headers = {}
}
options.headers.host = `${this.id}.localhost:${this.port}`
return request(`http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}${path}`, options)
timeout: 10000,
headers: {
...(options.headers || {}),
host: this.apiUrl.host,
}
})
}
protected async getConnectionStatus() {
@ -202,8 +208,8 @@ export class Cluster implements ClusterModel {
if (kubernetesVersion.includes("gke")) return "gke"
if (kubernetesVersion.includes("eks")) return "eks"
if (kubernetesVersion.includes("IKS")) return "iks"
if (apiUrl.endsWith("azmk8s.io")) return "aks"
if (apiUrl.endsWith("k8s.ondigitalocean.com")) return "digitalocean"
if (apiUrl.href.endsWith("azmk8s.io")) return "aks"
if (apiUrl.href.endsWith("k8s.ondigitalocean.com")) return "digitalocean"
if (contextName.startsWith("minikube")) return "minikube"
if (kubernetesVersion.includes("+")) return "custom"
return "vanilla"
@ -271,7 +277,7 @@ export class Cluster implements ClusterModel {
return toJS({
...storeModel,
url: this.url,
apiUrl: this.apiUrl,
apiUrl: this.apiUrl.href,
online: this.online,
accessible: this.accessible,
failureReason: this.failureReason,

View File

@ -1,6 +1,5 @@
import { CoreV1Api, KubeConfig } from "@kubernetes/client-node"
import { CoreV1Api } from "@kubernetes/client-node"
import { ServerOptions } from "http-proxy"
import * as url from "url"
import logger from "./logger"
import { getFreePort } from "./port"
import { KubeAuthProxy } from "./kube-auth-proxy"
@ -15,31 +14,21 @@ export class ContextHandler {
public contextName: string
protected id: string
protected clusterUrl: url.UrlWithStringQuery
protected proxyServer: KubeAuthProxy
protected apiTarget: ServerOptions
protected certData: string
protected authCertData: string
protected cluster: Cluster
protected apiTarget: ServerOptions
protected proxyTarget: ServerOptions
protected clientCert: string
protected clientKey: string
protected secureApiConnection = true
protected defaultNamespace: string
protected kubernetesApi: string
protected prometheusProvider: string
protected prometheusPath: string
protected clusterName: string
constructor(kc: KubeConfig, cluster: Cluster) {
constructor(protected cluster: Cluster) {
this.id = cluster.id
this.cluster = cluster
this.clusterUrl = url.parse(cluster.apiUrl)
this.url = cluster.apiUrl.href;
this.contextName = cluster.contextName;
this.defaultNamespace = kc.getContextObject(cluster.contextName).namespace
this.url = `http://${this.id}.localhost:${cluster.port}/`
this.kubernetesApi = `http://127.0.0.1:${cluster.port}/${this.id}`
this.setClusterPreferences(cluster.preferences)
}
@ -79,16 +68,12 @@ export class ContextHandler {
return await provider.getPrometheusService(apiClient)
})
const resolvedPrometheusServices = await Promise.all(prometheusPromises)
const service = resolvedPrometheusServices.filter(n => n)[0]
if (service) {
return service
} else {
return {
id: "lens",
namespace: "lens-metrics",
service: "prometheus",
port: 80
}
const service = resolvedPrometheusServices.filter(n => n)[0];
return service || {
id: "lens",
namespace: "lens-metrics",
service: "prometheus",
port: 80
}
}
@ -112,17 +97,18 @@ export class ContextHandler {
}
protected async newApiTarget(timeout: number): Promise<ServerOptions> {
const clusterUrl = this.cluster.apiUrl;
return {
changeOrigin: true,
timeout: timeout,
headers: {
"Host": this.clusterUrl.hostname
"Host": clusterUrl.hostname
},
target: {
port: await this.resolveProxyPort(),
protocol: "http://",
host: "localhost",
path: this.clusterUrl.path
path: clusterUrl.path,
},
}
}

View File

@ -5,7 +5,6 @@ import "../common/prometheus-providers"
import { app, dialog } from "electron"
import { appName, appProto, isMac, staticDir, staticProto } from "../common/vars";
import path from "path"
import { format as formatUrl } from "url"
import initMenu from "./menu"
import { LensProxy, listen } from "./proxy"
import { WindowManager } from "./window-manager";
@ -25,12 +24,6 @@ let windowManager: WindowManager;
let clusterManager: ClusterManager;
let proxyServer: LensProxy;
const vmURL = formatUrl({
pathname: path.join(__dirname, `${appName}.html`),
protocol: "file",
slashes: true,
})
mangleProxyEnv()
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
@ -81,9 +74,9 @@ async function main() {
app.quit();
}
// manage lens windows
windowManager = new WindowManager({showSplash: true});
windowManager.loadURL(vmURL)
// create window manager and open app
windowManager = new WindowManager();
windowManager.showSplash();
}
// Events
@ -95,19 +88,15 @@ app.on('window-all-closed', function () {
if (!isMac) {
app.quit();
} else {
windowManager = null
if (clusterManager) clusterManager.stop()
// windowManager.destroy();
// clusterManager.stop()
}
})
app.on("activate", () => {
if (!windowManager) {
windowManager = new WindowManager()
windowManager.loadURL(vmURL)
}
app.on("activate", (event, hasVisibleWindows) => {
// todo: something
})
// fixme: app can't quit normally (Cmd+W/Q not working)
app.on("will-quit", async (event) => {
event.preventDefault(); // To allow mixpanel sending to be executed
if (clusterManager) clusterManager.stop()

View File

@ -1,4 +1,4 @@
import k8s from "@kubernetes/client-node"
import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node"
import os from "os"
import yaml from "js-yaml"
import logger from "./logger";
@ -10,8 +10,8 @@ function resolveTilde(filePath: string) {
return filePath;
}
export function loadConfig(kubeConfigPath?: string): k8s.KubeConfig {
const kc = new k8s.KubeConfig()
export function loadConfig(kubeConfigPath?: string): KubeConfig {
const kc = new KubeConfig()
if (kubeConfigPath) {
kc.loadFromFile(resolveTilde(kubeConfigPath))
} else {
@ -27,38 +27,35 @@ export function loadConfig(kubeConfigPath?: string): k8s.KubeConfig {
* - Context
* @param config KubeConfig to check
*/
export function validateConfig(config: k8s.KubeConfig | string): k8s.KubeConfig {
if(typeof config == "string") {
export function validateConfig(config: KubeConfig | string): KubeConfig {
if (typeof config == "string") {
config = loadConfig(config);
}
logger.debug(`validating kube config: ${JSON.stringify(config)}`)
if(!config.users || config.users.length == 0) {
if (!config.users || config.users.length == 0) {
throw new Error("No users provided in config")
}
if(!config.clusters || config.clusters.length == 0) {
if (!config.clusters || config.clusters.length == 0) {
throw new Error("No clusters provided in config")
}
if(!config.contexts || config.contexts.length == 0) {
if (!config.contexts || config.contexts.length == 0) {
throw new Error("No contexts provided in config")
}
return config
}
/**
* Breaks kube config into several configs. Each context as it own KubeConfig object
*
* @param configString yaml string of kube config
*/
export function splitConfig(kubeConfig: k8s.KubeConfig): k8s.KubeConfig[] {
const configs: k8s.KubeConfig[] = []
if(!kubeConfig.contexts) {
export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
const configs: KubeConfig[] = []
if (!kubeConfig.contexts) {
return configs;
}
kubeConfig.contexts.forEach(ctx => {
const kc = new k8s.KubeConfig();
const kc = new KubeConfig();
kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n);
kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n)
kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n)
@ -74,13 +71,13 @@ export function splitConfig(kubeConfig: k8s.KubeConfig): k8s.KubeConfig[] {
*
* @param configPath path to kube config yaml file
*/
export function loadAndSplitConfig(configPath: string): k8s.KubeConfig[] {
const allConfigs = new k8s.KubeConfig();
export function loadAndSplitConfig(configPath: string): KubeConfig[] {
const allConfigs = new KubeConfig();
allConfigs.loadFromFile(configPath);
return splitConfig(allConfigs);
}
export function dumpConfigYaml(kc: k8s.KubeConfig): string {
export function dumpConfigYaml(kc: KubeConfig): string {
const config = {
apiVersion: "v1",
kind: "Config",
@ -128,10 +125,10 @@ export function dumpConfigYaml(kc: k8s.KubeConfig): string {
console.log("dumping kc:", config);
// skipInvalid: true makes dump ignore undefined values
return yaml.safeDump(config, {skipInvalid: true});
return yaml.safeDump(config, { skipInvalid: true });
}
export function podHasIssues(pod: k8s.V1Pod) {
export function podHasIssues(pod: V1Pod) {
// Logic adapted from dashboard
const notReady = !!pod.status.conditions.find(condition => {
return condition.type == "Ready" && condition.status !== "True"
@ -146,7 +143,7 @@ export function podHasIssues(pod: k8s.V1Pod) {
// Logic adapted from dashboard
// see: https://github.com/kontena/kontena-k8s-dashboard/blob/7d8f9cb678cc817a22dd1886c5e79415b212b9bf/client/api/endpoints/nodes.api.ts#L147
export function getNodeWarningConditions(node: k8s.V1Node) {
export function getNodeWarningConditions(node: V1Node) {
return node.status.conditions.filter(c =>
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
)

View File

@ -1,9 +1,9 @@
import { app } from "electron"
import fs from "fs"
import { ensureDir, randomFileName} from "./file-helpers"
import { ensureDir, randomFileName } from "./file-helpers"
import logger from "./logger"
import { Cluster } from "./cluster"
import * as k8s from "./k8s"
import { dumpConfigYaml } from "./k8s"
import { KubeConfig } from "@kubernetes/client-node"
export class KubeconfigManager {
@ -21,37 +21,35 @@ export class KubeconfigManager {
}
/**
* Creates new "temporary" kubeconfig that point to the kubectl-proxy.
* Creates new "temporary" kubeconfig that point to the kubectl-proxy.
* This way any user of the config does not need to know anything about the auth etc. details.
*/
protected createTemporaryKubeconfig(): string {
ensureDir(this.configDir)
ensureDir(this.configDir);
const path = `${this.configDir}/${randomFileName("kubeconfig")}`
const originalKc = new KubeConfig()
originalKc.loadFromFile(this.cluster.kubeConfigPath)
const kc = {
clusters: [
{
name: this.cluster.contextName,
server: `http://127.0.0.1:${this.cluster.contextHandler.proxyPort}`
}
],
users: [
{
name: "proxy"
}
],
contexts: [
{
name: this.cluster.contextName,
cluster: this.cluster.contextName,
namespace: originalKc.getContextObject(this.cluster.contextName).namespace,
user: "proxy"
}
],
currentContext: this.cluster.contextName
} as KubeConfig
fs.writeFileSync(path, k8s.dumpConfigYaml(kc))
const { contextName, contextHandler, kubeConfigPath } = this.cluster;
const kubeConfig = new KubeConfig()
kubeConfig.loadFromFile(kubeConfigPath)
kubeConfig.clusters = [
{
name: contextName,
server: `http://127.0.0.1:${contextHandler.proxyPort}`,
skipTLSVerify: true,
}
];
kubeConfig.users = [
{ name: "proxy" },
];
kubeConfig.currentContext = contextName;
kubeConfig.contexts = [
{
name: contextName,
cluster: contextName,
namespace: kubeConfig.getContextObject(contextName).namespace,
user: "proxy"
}
];
fs.writeFileSync(path, dumpConfigYaml(kubeConfig));
return path
}

View File

@ -4,14 +4,10 @@ import http from "http"
import path from "path"
import { readFile } from "fs-extra"
import { Cluster } from "./cluster"
import { configRoute } from "./routes/config"
import { helmApi } from "./helm-api"
import { resourceApplierApi } from "./resource-applier-api"
import { kubeconfigRoute } from "./routes/kubeconfig"
import { metricsRoute } from "./routes/metrics"
import { watchRoute } from "./routes/watch"
import { portForwardRoute } from "./routes/port-forward"
import { apiPrefix, outDir, appName } from "../common/vars";
import { apiPrefix, appName, outDir } from "../common/vars";
import { configRoute, kubeconfigRoute, metricsRoute, portForwardRoute, watchRoute } from "./routes";
const mimeTypes: Record<string, string> = {
"html": "text/html",
@ -51,13 +47,13 @@ export class Router {
}
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse) {
const url = new URL(req.url, "http://localhost")
const path = url.pathname
const reqUrl = new URL(req.url, "http://localhost")
const path = reqUrl.pathname
const method = req.method.toLowerCase()
const matchingRoute = this.router.route(method, path)
if (matchingRoute.isBoom !== true) { // route() returns error if route not found -> object.isBoom === true
const request = await this.getRequest({ req, res, cluster, url, params: matchingRoute.params })
const request = await this.getRequest({ req, res, cluster, url: reqUrl, params: matchingRoute.params })
await matchingRoute.route(request)
return true
} else {

5
src/main/routes/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from "./config-route"
export * from "./kubeconfig-route"
export * from "./metrics-route"
export * from "./port-forward-route"
export * from "./watch-route"

View File

@ -12,7 +12,7 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
{
'name': cluster.contextName,
'cluster': {
'server': cluster.apiUrl,
'server': cluster.apiUrl.href,
'certificate-authority-data': secret.data["ca.crt"]
}
}

View File

@ -1,8 +1,7 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import requestPromise from "request-promise-native"
import { PrometheusProviderRegistry, PrometheusProvider, PrometheusNodeQuery, PrometheusClusterQuery, PrometheusPodQuery, PrometheusPvcQuery, PrometheusIngressQuery, PrometheusQueryOpts} from "../prometheus/provider-registry"
import { apiPrefix } from "../../common/vars";
import { PrometheusClusterQuery, PrometheusIngressQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusProvider, PrometheusPvcQuery, PrometheusQueryOpts } from "../prometheus/provider-registry"
export type IMetricsQuery = string | string[] | {
[metricName: string]: string;
@ -11,11 +10,10 @@ export type IMetricsQuery = string | string[] | {
class MetricsRoute extends LensApi {
public async routeMetrics(request: LensApiRequest) {
const { response, cluster} = request
const { response, cluster } = request
const query: IMetricsQuery = request.payload;
const serverUrl = `http://127.0.0.1:${cluster.port}${apiPrefix.KUBE_BASE}`
const headers = {
"Host": `${cluster.id}.localhost:${cluster.port}`,
const headers: Record<string, string> = {
"Host": cluster.apiUrl.host,
"Content-type": "application/json",
}
const queryParams: IMetricsQuery = {}
@ -27,7 +25,7 @@ class MetricsRoute extends LensApi {
let prometheusProvider: PrometheusProvider
try {
const prometheusPath = await cluster.contextHandler.getPrometheusPath()
metricsUrl = `${serverUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range`
metricsUrl = `${cluster.apiServerUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range`
prometheusProvider = await cluster.contextHandler.getPrometheusProvider()
} catch {
this.respondJson(response, {})
@ -65,11 +63,9 @@ class MetricsRoute extends LensApi {
let data: any;
if (typeof query === "string") {
data = await loadMetrics(query)
}
else if (Array.isArray(query)) {
} else if (Array.isArray(query)) {
data = await Promise.all(query.map(loadMetrics));
}
else {
} else {
data = {};
const result = await Promise.all(
Object.entries(query).map((queryEntry: any) => {

View File

@ -1,133 +1,101 @@
import { BrowserView, BrowserWindow, shell } from "electron"
import { reaction } from "mobx";
import { BrowserWindow, shell } from "electron"
import windowStateKeeper from "electron-window-state"
import type { ClusterId } from "../common/cluster-store";
import { clusterStore } from "../common/cluster-store";
import { tracker } from "../common/tracker";
export interface WindowManagerParams {
showSplash?: boolean;
}
export class WindowManager {
protected mainWindow: BrowserWindow;
protected splashWindow?: BrowserWindow;
protected windowState: windowStateKeeper.State;
protected views = new Map<ClusterId, BrowserView>();
protected disposers: Function[] = [];
protected views = new Map<ClusterId, BrowserWindow>();
protected disposers = this.bindReactions();
constructor(protected params: WindowManagerParams = {}) {
this.params = { showSplash: false, ...params };
protected splashWindow = new BrowserWindow({
width: 500,
height: 300,
backgroundColor: "#1e2124",
center: true,
frame: false,
resizable: false,
show: false,
});
// Manage main window size and position with state persistence
this.windowState = windowStateKeeper({
defaultHeight: 900,
defaultWidth: 1440,
});
// Manage main window size and position with state persistence
protected windowState = windowStateKeeper({
defaultHeight: 900,
defaultWidth: 1440,
});
this.mainWindow = new BrowserWindow({
show: false,
x: this.windowState.x,
y: this.windowState.y,
width: this.windowState.width,
height: this.windowState.height,
backgroundColor: "#1e2124",
titleBarStyle: "hidden",
webPreferences: {
nodeIntegration: true,
},
});
// Splash-screen window with loading indicator
this.splashWindow = new BrowserWindow({
width: 500,
height: 300,
backgroundColor: "#1e2124",
center: true,
frame: false,
resizable: false,
show: false,
});
this.splashWindow.loadURL("static://splash.html")
// Hook window state manager into window lifecycle
this.windowState.manage(this.mainWindow);
// Disallow closing main window
this.mainWindow.on("close", (evt) => {
evt.preventDefault();
});
// Open external links in default browser (target=_blank, window.open)
this.mainWindow.webContents.on("new-window", (event, url) => {
event.preventDefault();
shell.openExternal(url);
});
// Track main window focus
this.mainWindow.on("focus", () => {
tracker.event("app", "focus")
});
// Clean up views for removed clusters
this.disposers.push(
protected bindReactions() {
return [
// auto-destroy cluster-view when it's removed
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
removedClusters.forEach(cluster => {
const lensView = this.getView(cluster.id);
if (lensView) {
lensView.destroy();
this.views.delete(cluster.id);
}
this.destroyView(cluster.id);
});
})
);
]
}
async loadURL(url: string) {
if (this.params.showSplash) {
this.splashWindow.show();
}
await this.mainWindow.loadURL(url);
this.mainWindow.show();
async showSplash() {
await this.splashWindow.loadURL("static://splash.html")
this.splashWindow.show();
}
hideSplash() {
this.splashWindow.hide();
this.setView("cluster-id-blabla");
}
async setView(clusterId: ClusterId) {
const view = this.getView(clusterId)
this.mainWindow.addBrowserView(view);
// await view.webContents.loadURL("http://ya.ru");
// view.setBounds({
// x: 10,
// y: 10,
// width: this.windowState.width - 20,
// height: this.windowState.height - 20,
// })
// view.setAutoResize({ horizontal: true, vertical: true });
protected async showView(clusterId: ClusterId) {
const cluster = clusterStore.getById(clusterId);
if (!cluster) {
throw new Error(`Can't load view for non-existing cluster="${clusterId}"`);
}
const view = this.getView(clusterId);
const url = cluster.apiUrl.href;
if (view.webContents.getURL() !== url) {
await view.loadURL(url);
}
view.show();
}
getView(clusterId: ClusterId): BrowserView {
getView(clusterId: ClusterId) {
let view = this.views.get(clusterId);
if (!view) {
view = new BrowserView({
view = new BrowserWindow({
show: false,
x: this.windowState.x,
y: this.windowState.y,
width: this.windowState.width,
height: this.windowState.height,
backgroundColor: "#1e2124",
titleBarStyle: "hidden",
webPreferences: {
nodeIntegration: true
}
})
nodeIntegration: true,
},
});
// open external links in default browser (target=_blank, window.open)
view.webContents.on("new-window", (event, url) => {
event.preventDefault();
shell.openExternal(url);
});
this.views.set(clusterId, view);
}
return view;
}
destroyView(clusterId: ClusterId) {
const view = this.views.get(clusterId);
if (view) {
view.destroy();
this.views.delete(clusterId);
}
}
destroy() {
this.disposers.forEach(dispose => dispose());
this.disposers.length = 0;
this.views.forEach(view => view.destroy());
this.views.clear();
this.mainWindow.destroy();
this.splashWindow.destroy();
this.mainWindow = null;
this.splashWindow = null;
}
}

View File

@ -1,5 +1,5 @@
// App configuration api
import type { IConfigRoutePayload } from "../../../main/routes/config";
import type { IConfigRoutePayload } from "../../../main/routes/config-route";
import { apiBase } from "../index";
export const configApi = {

View File

@ -2,7 +2,7 @@
import moment from "moment";
import { apiBase } from "../index";
import type { IMetricsQuery } from "../../../main/routes/metrics";
import type { IMetricsQuery } from "../../../main/routes/metrics-route";
export interface IMetrics {
status: string;

View File

@ -1,4 +1,4 @@
import type { IConfigRoutePayload } from "../main/routes/config";
import type { IConfigRoutePayload } from "../main/routes/config-route";
import { observable, when } from "mobx";
import { autobind, interval } from "./utils";